Estadística, ciencia de datos y análisis de datos

El objetivo de este primer capítulo es introducir algunos términos y conceptos generales de estadística, que nos serán de utilidad para discutir todos los tópicos de este curso y de posteriores cursos de este Programa de Actualización en Ciencia de Datos. La introducción es bastante general, por lo que a lo largo del texto se proveen de enlaces para seguir leyendo y profundizando en el tema.

Estadística y ciencia de datos ¿Dos nombres para lo mismo?

La estadística es el estudio de la recolección, análisis, interpretación y organización de datos. El corolario de esta definición podría ser que además de ser una disciplina científica en si misma, la estadística es una disciplina auxiliar de todas las demás ciencias.

La estadística suele tener el mote de ser una disciplina obscura en el mejor de los casos y de una forma sofisticada de mentir en el peor de ellos. Sin embargo, en los últimos años ha comenzado a emerger una disciplina llamada ciencia de datos (data science en inglés), para muchos no es más que un nuevo y sexy nombre para llamar a la vieja estadística, algo así como una campaña mediática de mejoramiento de imagen. Para otros la ciencia de datos es un aporte valioso que si bien no es exactamente estadística, está contribuyendo a ésta con nuevos métodos, aproximaciones, preguntas, recursos ($$) y por supuesto mucha gente interesada en estos temas.

Una posible definición de la ciencia de datos y su relación con la estadística se muestra en el siguiente diagrama de Venn.

Según el diagrama, la estadística es una de las partes de la ciencia de datos. La gran diferencia entre la investigación tradicional y la ciencia de datos radica no en los conocimientos estadísticos (que ambas requieren) si no en las habilidades de hacking. En esto contexto hacking no hace referencia a la capacidad de vulnerar la seguridad de computadoras ajenas, si no a la capacidad técnica y creativa para encontrar soluciones mediante el uso de código.

Entonces, para empezar a hacer ciencia de datos necesitamos (además de un problema!):

  • Nociones básicas de programación.
  • Nociones básicas de estadística.

En este curso aprenderemos ambas en paralelo. Esto puede sonar complicado, pero en realidad ambos aprendizajes se acompañan y apuntalan mutuamente. La mejor forma de probar que esto es cierto es hacer el curso, pero como adelanto podemos decir que esto funciona debido a que:

  • Aprender a programar sin tener un problema que resolver puede ser, para la mayoría de las personas, una tarea demasiado abstracta y poco placentera. El aprendizaje de la estadística nos proveerá de estos problemas.

  • La conceptos estadísticos tienen una fundamentación teórica que suele requerir de cierta formación matemática que no es común a la mayoría de los científicos. El manejo de un lenguaje de programación provee una ruta alternativa (o aún mejor complementaria), ya que nos permite comprender conceptos vía la simulación/experimentación.

Volviendo al diagrama de Venn, tanto la investigación tradicional como la ciencia de datos necesitan ir acompañadas de conocimiento sustantivo, esto hace referencia al conocimiento de una área particular del saber. La estadística puede ser de ayuda para estudiar genomas o partículas elementales, pero para poder hacer preguntas relevantes (y entender las respuestas) primero hay que comprender qué son los genomas y qué son las partículas elementales. La estadística NO es una máquina auto-mágica por donde entran datos en crudo por un lado y sale información por el otro (aunque a veces se publicite de esa forma). La estadística es una herramienta que nos ayuda a pensar y tomar decisiones de forma adecuada, pero requiere del conocimiento, el criterio y la responsabilidad de quien la usa.

Python

El lenguaje de programación elegido para este curso es Python.

Python es:

  • Es un lenguaje simple: El código es simple de leer, de escribir y de mantener.
  • De propósito general. Se suele decir que Python no es el mejor lenguaje para casi nada, pero es suficientemente bueno para casi todo (análisis de datos, páginas web, simulaciones, enseñanza, juegos, música, etc).
  • Multiparadigma. Es posible programar usando distintos estilos de programación incluso combinándolos.
  • De alto nivel. Es decir más cercano al lenguaje humano que al lenguaje de máquinas.
  • Es interpretado. No es necesario compilarlo antes de correrlo, por lo que se puede usar de forma interactiva.
  • Es multiplataforma. Corre en diversos sistemas operativos.
  • Es gratuito y de código abierto.
  • Está muy bien documentado.
  • Es ampliamente usado en la mayoría de las disciplinas científicas
  • Tiene una gran comunidad de usuarios (no todos científicos), por lo que es fácil encontrar ayuda, tutoriales, foros, blogs, etc. Por ejemplo en StackOverflow o en telegram (@Python_cientifico).

Es posible usar Python para crear programas complejos que ocupan cientos o miles de lineas de texto. También es posible usar Python de forma interactiva. En este curso nos focalizaremos en esta segunda forma, para la cual las notebooks de Jupyter son ideales.

Análisis exploratorio de datos

Para poder empezar a analizar datos lo que primero que hay que tener es ...datos! Estos datos provienen de experimentos, simulaciones, encuestas, observaciones, búsquedas en base de datos, etc. Rara vez los datos se nos presentan de forma inmaculada y lista para usar, por lo general hay que limpiarlos, procesarlos, combinarlos con datos de otras fuentes etc. Para sorpresa y frustración (sobre todo frustración) de quienes se inician en el análisis de datos esta etapa suele ser la que más tiempo involucra. En este curso veremos algunos rudimentos básicos de como hacer estas tareas.

Suponiendo que ya tenemos nuestros datos, lo recomendable es intentar ganar intuición sobre los datos que tenemos enfrente, tratar de ver qué dicen de nuestro problema, si es que dicen algo. Para ello se han desarrollado una colección de métricas y métodos colectivamente llamados Análisis exploratorio de datos (EDA por sus siglas en inglés), la cual se compone básicamente de dos herramientas complementarias:

  1. La estadística descriptiva
  2. La visualización de datos

La primera se ocupa de describir de forma cuantitativa un conjunto de datos. Para ello se recurre a un conjunto de medidas que nos ayudan a resumir los datos en unos pocos números. En general se habla de medidas de centralidad, de dispersión. La estadística descriptiva no intenta establecer conclusiones más allá de los datos disponibles, solo se limita a describir la muestra (o dataset) sin intentar decir nada de la población de la cual provienen esos datos, es decir no hace ni generalizaciones ni inferencias.

La segunda se encarga de mostrar los datos de forma que sea más fácil interpretarlos. Los seres humanos tenemos un sistema visual muy poderoso y la visualización de datos intenta aprovechar sus virtudes a la vez que mitigar sus defectos.

Tipos de variables

Tanto en análisis de datos como en programación se habla de variables, aunque el significado no es exactamente el mismo, veamos:

En estadística una variable es simplemente una cantidad que puede tomar un valor a partir de un conjunto de valores permitidos. Las variables se suelen clasificar de la siguiente manera (aunque hay otras clasificaciones):

  • Métricas o cuantitativas: Son variables con las cuales es posible establecer un orden y computar distancias. Cuando en la escala existe un cero es posible además calcular proporciones, por ejemplo 1 hora es la mitad de 2 horas (por que 0 horas implica la ausencia de tiempo transcurrido). En cambio $40 ^\circ \text{C}$ no es el doble de $20 ^\circ \text{C}$, por que el cero de la escala Celcius es un punto totalmente arbitrario (contrario al cero de la escala Kelvin).

    • Continuas: Se describen usando el conjunto de los reales, algunos ejemplos son la temperatura, altura, peso, etc.
    • Discretas: Se describen usando el conjunto de los enteros, cantidad de hijos, de aviones de una aerolínea, etc.
  • Cualitativas: Son el tipo de variables que indican cualidades, o atributos.

    • Categóricas o nominales: Son variables que indican pertenencia a categorías mutuamente excluyentes, como cara o ceca en una moneda. Aún cuando se usen números como 1 y 0 para representar este tipo de variables no se puede establecer orden alguno, "cara" no es más o menos que "ceca", ni "ceca" viene antes que "cara". Las variables categóricas no tienen por que ser dos, pueden ser millones, como los colores.
    • Ordinales: La variable puede tomar distintos valores ordenados siguiendo una escala establecida, aunque el intervalo no tiene por que ser uniforme, por ejemplo en una carrera de bicicletas, la distancia entre la primer y segunda competidora no tiene por que ser la misma que entre la segunda y la tercera.

En Python, como en otros lenguajes de programación, se le llama variable a un espacio en la memoria de la computadora que almacena un valor determinado y que tiene asociado un identificador, es decir un rótulo o nombre. Existen distintos "tipos" de variable, en Python los tipos más comunes que encontraremos serán:

  • int : que corresponde a los números enteros. Por ej 42, 1, -4.
  • float : son una aproximación de los números reales por ej 3.14, 1.0, -0.124.
  • string : que corresponde a "letras" o cadenas de letras y se escriben usando comillas dobles "" o simples ''. Por ejemplo 'hola', '42', 's'.
  • bool : Se corresponde con los booleanos. Sólo hay dos valores posibles True y False.

Para saber el tipo de una variable en Python podemos usar la función type():


In [1]:
type(42)


Out[1]:
int

In [2]:
type(42.0)


Out[2]:
float

In [3]:
type("42")


Out[3]:
str

In [4]:
type(True)


Out[4]:
bool

Existen muchos otros "tipos" de variables en Python, incluso los usuarios avanzados pueden crear sus propios tipos de variables! Por ahora estos tipos son suficientes. Tener variables es útil por que con ellas podemos hacer operaciones, por ejemplo operaciones matemáticas.


In [5]:
1 + 1 # esto devuelve un entero


Out[5]:
2

In [6]:
1. + 1  # esto devuelve un float


Out[6]:
2.0

In [7]:
2 / 1  # en Python3 la división siempre devuelve floats


Out[7]:
2.0

In [8]:
"42" + 1  # esta operación no tiene sentido y por lo tanto Python devuelve un error!


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-8-5cfddbfa6c9c> in <module>()
----> 1 "42" + 1  # esta operación no tiene sentido y por lo tanto Python devuelve un error!

TypeError: must be str, not int

In [9]:
'4' * 4  # esta operación SI tiene sentido en Python, es lo que esperabas?


Out[9]:
'4444'

In [10]:
1 > 2  # acá comparamos variables y obtenemos un booleano


Out[10]:
False

In [11]:
2 == 2  # ojo que el operador "igualdad" es "==" y no "="


Out[11]:
True

En los ejemplos anteriores hemos realizado algunas operaciones con variables, pero no las hemos guardado en ningún lado. Puede no ser obvio al principio pero para poder hacer tareas relativamente complejas es necesario poder guardar variables y darles nombres (o identificador). De esa forma podemos realizar una operación, como por ejemplo 2 + 2 y guardar el resultado bajo algún nombre para luego usarlo cuando lo necesitemos.

Los nombres de las variables en Python siguen ciertas reglas que es necesario respetar.

  • Deben comenzar con letras o con un _
  • La letras pueden corresponderse con los caracteres a-z, A-Z
  • También podemos usar numeros 0-9 (solo que no como primer carácter).

Python 3 permite además usar otros caracteres, como por ejemplo letras griegas, la ñ, caracteres acentuados, etc; aunque el uso de este tipo de caracteres para nombrar variables no esta muy difundido aún.

Por convención, los nombres de las variables comienzan con una letra minúscula al igual que los nombres de las funciones (ver más adelante). Los nombres que comienzan con mayúsculas están reservados para las clases, en este curso no vamos a estudiar clases.

Para asignarle un valor a una variable se usa el símbolo =


In [12]:
a = 2 + 3

En la celda anterior estamos diciendole a Python: "Tome el valor 2 súmele el valor 3 y guárdelo en la variable que se llama a".

Esto lo podemos comprobar al ejecutar:


In [13]:
a


Out[13]:
5

No solo es posible usar variables para guardar valores, también podemos operar con ellas. Por ejemplo podemos preguntar si nuestra variable es mayor que cierto valor.


In [14]:
a > 2


Out[14]:
True

O podemos restarle un valor


In [15]:
a - 1


Out[15]:
4

Incluso podemos actualizar el valor de las variables, por ej.


In [16]:
a = a + 1
a


Out[16]:
6

El ejemplo anterior muestra que el signo = no es el operador igualdad (que como ya vimos es ==). No es correcto decir que a es igual a la expresión a + 1. Aunque si nos expresamos de esa forma lo más probable es que nos entiendan y nadie nos diga nada.

Si tuvieramos que leer la celda anterior en voz alta diríamos: "tome el valor de la variable a, súmele 1 y guarde el resultado en la variable a".

Si tratamos de usar una variable que no ha sido definida previamente obtendremos un mensaje de error:


In [17]:
z


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-17-3a710d2a84f8> in <module>()
----> 1 z

NameError: name 'z' is not defined

Los errores son parte central de la programación y hay que acostumbrarse a cometerlos ya que es así como se avanza en la escritura de un programa. Al producirse errores, Python entrega mensajes que son muy informativos y por lo tanto útiles para solucionar el error, por lo que es muy beneficioso aprender a interpretarlos y prestarles mucha atención cuando ocurren.

El proceso de corrección de errores de un programa se llama debugging y es quizá una de las tareas más demandantes al escribir código. Python fue pensado como un lenguaje fácil de leer debido a que en general uno pasa más tiempo leyendo código (para arreglar los errores) que escribiéndolo. Mitad broma, mitad en serio se dice que si el debugging es el proceso por el cual se eliminan errores la programación debe ser el proceso por el cual se introducen los errores.

Nota: en muchos lenguajes de programación (como C/C++ o Fortran) antes de poder asignar valores a variables es necesario declararlas. Declararlas, quiere decir que tenemos que indicar que nuestro programa usará una variable de nombre tal que será del tipo cual. Recién una vez declarada la variable podemos asignarle valores concretos. En Python esto no es necesario, además en Python está permitido cambiar el tipo de una variable, por ejemplo podemos usar la variable a que hacía referencia a enteros int para referirnos a un string. El nombre técnico de esto es el de tipado dinámico (el tipo de una variable no es fijo, si no dinámico).


In [18]:
a = '42'
a


Out[18]:
'42'

Puede parecer poco lo que hemos aprendido hasta ahora, pero ya sabemos suficiente Python como para hacer algunos cálculos. Nada mejor para aprender a programar que intentar solucionar problemas. Un problema que podemos resolver con lo que sabemos de Python es calcular el valor de la media de un conjunto de valores.

Una forma fácil de perder tiempo al intentar resolver problemas es no tener demasiado claro cual es el problema que se intenta solucionar, asi que antes de calcular la media veamos primero como se define.

Medidas de centralidad

Media

Uno de los cómputos más elementales en estadística consiste en calcular la media, también conocido como promedio o valor esperado. Existen varias expresiones matemáticas que nos permiten calcular la media, quizá la más común sea:

$$E[\mathbf{x}] = \frac{1}{n} \sum_{i=1}^n{x_i}$$

Es decir, para obtener la media de $n$ valores, los sumamos y luego dividimos en $n$. En estadística se suele usar la notación $E[\mathbf{x}]$ nomo una forma abreviada de referirse a la media (o valor esperado) de $\mathbf{x}$.

Es común en estadística distinguir entre la media de una muestra, es decir de un conjunto finito de datos (que se suele simbolizar con el símbolo ${}^\bar{}$ por ejemplo $\bar x$) y la media de la población (que se suele simbolizar con la letra griega $\mu$), en la mayoría de los casos la población es un objeto imaginario al que no tenemos acceso y que solo aproximamos, en la gran mayoría de los casos $\bar x$ es un buen estimador de $\mu$, por ejemplo a medida que juntemos más y más datos el valor de $\bar x$ se aproximará al de $\mu$.

Dado un conjunto de valores $\mathcal{D} = \{1, 2, 3, 4, 5, 6\}$, una forma de calcular la media, usando Python, es:


In [19]:
media = (1 + 2 + 3 + 4 + 5 + 6) / 6
media


Out[19]:
3.5

El código que acabamos de escribir no difiere mucho de lo que podríamos haber realizado con una calculadora, primero tenemos que ingresar los numeros a mano y además tenemos que saber exactamente la cantidad de números que estamos sumando para luego saber por que cantidad dividir. Usando un lenguaje de programación podemos hacer algo bastante más cómodo. Pero para ello tenemos que aprender un par de conceptos nuevos.

Listas

Las listas de Python son un tipo de objeto que permite almacenar otros objetos, por ejemplo números. Gran parte de la programación en Python implica crear y manipular listas.

La sintaxis para crear listas en Python es [..., ..., ...]:


In [20]:
lista = [] # crea una lista vacia
lista


Out[20]:
[]

In [21]:
num = [1, 2, 3, 4, 5, 6]
num, type(num)


Out[21]:
([1, 2, 3, 4, 5, 6], list)

Las listas son un tipo particular de lo que se conoce genéricamente como estructuras de datos, es decir una forma particular de organizar y almacenar datos de forma que sea conveniente trabajar con esos datos. Las listas de Python son convenientes por que permiten no solo almacenar valores sino realizar muchas otras operaciones de forma sencilla. Una operación común es preguntarle a una lista cual es su longitud, es decir cuantos elementos contiene. Esto se hace con la función len().


In [22]:
len(lista)


Out[22]:
0

In [23]:
len(num)


Out[23]:
6

Otra operación conveniente es la de sumar todos los valores de una lista, esto lo hacemos usando la función sum().


In [24]:
sum(num)


Out[24]:
21

Con estas dos nuevas funciones que hemos aprendido podemos re-escribir el cálculo de una media de la siguiente forma:


In [25]:
media = sum(num) / len(num)
media


Out[25]:
3.5

Esta nueva forma de calcular la media tiene varias ventajas. Una de ellas es que resulta más automática que la anterior, cada vez que cambiemos los valores contenidos en la lista num podremos ejecutar la celda anterior y obtener el valor de la media. Este es un buen momento para probar que esto es cierto.

El código de la celda anterior es mucho más general que nuestra primer versión del cómputo de la media. Pero tiene un inconveniente, veamos. Es común al escribir código que necesitemos repetir una operación muchas veces, una solución simple sería copiar y pegar el código cada vez que necesitemos calcular una media. El problema con esta aproximación es que es tediosa y muy propensa a cometer errores, pensemos que operaciones más complejas podrían requerir de cientos de líneas de código, no una sola como el ejemplo de la media. Una forma de resolver nuestro problema sería crear una función que calcule la media, de tal forma que podamos llamar a esa función cada vez que la necesitemos. En Python crear funciones es muy simple, veamos:


In [26]:
def calcular_media(a):
    res = sum(a) / len(a)
    return res
  1. En la primer linea podemos ver que para definir una función necesitamos la palabra reservada def. Le llamamos reservada ya que tiene significado especial para Python y no deberíamos usar ese nombre para otra cosa que no sea definir funciones.
  2. A continuación de def, y en la misma linea, va el nombre de la función, que puede ser el que más nos guste (siempre que respetemos las reglas para nombrar funciones, que son las mismas que para nombrar variables y que ya vimos más arriba).
  3. Entre paréntesis los nombres de los argumentos de la función. En este ejemplo a.
  4. Cerramos la primer línea con dos puntos :.
  5. La siguientes líneas contienen las operaciones que realiza la función, en este caso es una sola, pero potencialmente podrían ser decenas o cientos de líneas.
  6. Por último tenemos otra palabra reservada return seguida del valor que devuelve la función, es decir del resultado de la función. Esta ultima línea es opcional ya que es posible definir funciones que no devuelvan valor alguno (ya veremos ejemplos).

Es importante notar un detalle, muy particular de Python, todo el contenido de la función (lineas 2 y 3) está escrito usando una sangría de 4 espacios, la sangría es obligatoria y le indica a Python cual es el cuerpo de la función. Los 4 espacios son solo una convención, podrían ser 2, 3, 8, etc, lo importante es respetar la misma cantidad.

Una vez definida una función se la puede llamar usando su nombre y pasándole los argumentos necesarios.


In [27]:
calcular_media(num)


Out[27]:
3.5

Algo para prestar atención, el nombre de la variable que le pasamos a nuestra función no tiene nada que ver con el nombre del argumento de la función. Es decir nosotros le pasamos a la función un objeto llamado num, internamente calcular_media le asigna a ese objeto el alias a. Técnicamente decimos que a es una variable local de la función calcular_media, es decir a solo existe dentro de esta función, fuera de ella a podría no existir o ser una variable completamente distinta.


In [28]:
calcular_media([1, 3, 4]), a  # dentro de la función `a` contiene una lista, fuera el valor 6


Out[28]:
(2.6666666666666665, '42')

Otra convención en Python es escribir las funciones incluyendo un docstring. Que no es más que un string que se escribe a partir de la segunda línea de la función y que Python ignora por completo al ejecutar la función. La información contenida en el docstring no es para Python, es para los humanos que lo usan! El docstring consiste en una explicación sobre las operaciones que realiza una función, qué valores espera la función como entrada y qué valores devuelve (si es que devuelve algo). Los docstring se escriben usando comillas triples """ esto permite que el docstring tenga varias lineas. El estilo exacto de los docstring varía, pero la mayoría de las funciones en Python tratan de mantener un mismo estilo para sus docstrings. Un ejemplo de docstring sería:


In [29]:
def calcular_media(a):
    """
    Calcula la media partir de una lista.
    
    Parametros
    ----------
    a : lista
        Contiene los valores a promediar
    
    Resultado
    ----------
    res : float
        La media de los valores contenidos en `a`
    """
    res = sum(a) / len(a)
    return res

Entonces, la forma general de un docstring sería:


In [30]:
"""
Descripción, ¿para qué sirve la función? ¿qué tarea realiza?

Parametros
----------
nombre : tipo 
    descripción

Resultado
----------
nombre : tipo
    descripción
""";

El docstring no solo puede ser leído directamente del código, si no que puede ser usado por Python y por varias herramientas externas. Por ejemplo la función help() de Python ofrece ayuda sobre una función al usuario "mostrando el docstring". Por ejemplo, si quisieramos ver qué hace la función len()


In [31]:
help(len)


Help on built-in function len in module builtins:

len(obj, /)
    Return the number of items in a container.

Esto no solo sirve para funciones que ya vienen con Python, si no que el mismo mecanismo es usado para funciones definidas por los usuarios.


In [32]:
help(calcular_media)


Help on function calcular_media in module __main__:

calcular_media(a)
    Calcula la media partir de una lista.
    
    Parametros
    ----------
    a : lista
        Contiene los valores a promediar
    
    Resultado
    ----------
    res : float
        La media de los valores contenidos en `a`

Jupyter (lo que estamos usando para mostrar este documento y ejecutar el código), también permite acceder al docstring usando shift + TAB


In [33]:
calcular_media  # seleccioná esta celda y presioná shift + tab


Out[33]:
<function __main__.calcular_media>

Dentro de Jupyter también se puede acceder a la ayuda escribiendo ? o ?? luego de la función y presionado enter.

Todo muy bien hasta ahora, pero ¿Qué pasaría si inadvertidamente le pasáramos una lista vacía a nuestra función?


In [34]:
calcular_media([])


---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-34-130eb69d826e> in <module>()
----> 1 calcular_media([])

<ipython-input-29-1321a655ccc3> in calcular_media(a)
     13         La media de los valores contenidos en `a`
     14     """
---> 15     res = sum(a) / len(a)
     16     return res

ZeroDivisionError: division by zero

Obtenemos un error! Antes de seguir leyendo intentemos entender cual es la causa del error y en que parte del código se encuentra.




Como se puede ver el problema se debe a que la división por 0 no está definida. Este es un tipo de error que no se debe a que metimos la pata al programar el cálculo de la media, se debe a una imposibilidad matemática. Es, si lo pensamos, un error esperado algo que es razonable que ocurra, en la jerga pitónica decimos que es una excepción. Python permite lidiar con las excepciones de forma elegante (en vez de dejar que el programa falle horriblemente). Veamos primero el código y luego la explicación.


In [35]:
def calcular_media2(a):
    """
    Calcula la media partir de una lista.
    
    Parametros
    ----------
    a : lista
        lista que contiene los valores a promediar
    
    Resultado
    ----------
    res : float
        La media de los valores contenidos en `a`
        Devuelve una advertencia si `a` está vacía.
    """
    try:
        res = sum(a) / len(a)
        return res
    except ZeroDivisionError:
        print('la lista está vacía, por favor use una lista con números')

In [36]:
calcular_media2([])


la lista está vacía, por favor use una lista con números

Ahora la función en vez de fallar devuelve un mensaje. La novedad es que usamos el bloque try-except. Lo que hace esto es intentar correr lo que está dentro de cuerpo de try, si llegara a ocurrir un error, que en este caso hemos especificado que sea del tipo ZeroDivisionError, entonces se ejecuta lo que sea que esté dentro del bloque except en este caso un mensaje, pero podría ser otra cualquier cosa. Si hubieramos escrito solo except: (sin especificar ZeroDivisionError) entonces el bloque except se ejecutaría sin importar el tipo de error, esto si bien es legal no se recomienda ya que puede llegar a ocultar bugs. Por ejemplo sum(), y tambiénlen(), devuelven un error si lo usáramos con un entero en vez de una lista.


In [37]:
calcular_media2(1)


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-37-7cecaac34ae1> in <module>()
----> 1 calcular_media2(1)

<ipython-input-35-745b5996dc24> in calcular_media2(a)
     15     """
     16     try:
---> 17         res = sum(a) / len(a)
     18         return res
     19     except ZeroDivisionError:

TypeError: 'int' object is not iterable

Mediana

La media es una buena descripción si los datos que estamos midiendo son más o menos similares, pero puede ofrecer una visión muy distorsionada si los datos no son muy similares entre si, por ejemplo como puede suceder con los ingresos, algunas personas apenas ganan unos pocos pesos mientras que otras acumulan millones por mes.

La mediana es el número que separa un conjunto de datos en una mitad superior y otra inferior. La mediana es una medida más robusta que la media a valores extremos. Veamos un ejemplo para el conjunto $\mathcal{D} = \{1,2,3,4,5\}$, la media y la mediana es igual a 3. Si ahora agregaramos a este conjunto un nuevo valor, por ejemplo 100. La media pasará a ser $\eqsim 19,2$ mientras que la mediana apenas cambiará a 3.5 $\left ( \frac{3 + 4}{2} \right )$.

Para calcular la mediana necesitamos algunos conceptos nuevos. Una característica de las listas de Python es que es posible acceder a los elementos contenidos en ellas mediante índices. Los índices deben ser enteros, empiezan en 0 y terminan en len(.)-1.


In [38]:
lista = [5, 4, 3, 2, 1]
lista[0]  # el cero-ésimo elemento de la lista


Out[38]:
5

In [39]:
lista[2]


Out[39]:
3

In [40]:
lista[5]  # este índice no existe en este caso y Python nos lo indica con un error


---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-40-cfe6888f2d25> in <module>()
----> 1 lista[5]  # este índice no existe en este caso y Python nos lo indica con un error

IndexError: list index out of range

Los índices puede ser negativos


In [41]:
lista[-1]  # devuelve el último elemento


Out[41]:
1

No solo es posible acceder a elementos individuales de una lista, también se puede acceder a rebanadas (slices).


In [42]:
lista[1:]  # del elemento 1 al final


Out[42]:
[4, 3, 2, 1]

In [43]:
lista[1:4]  # del elemento 1 al 4


Out[43]:
[4, 3, 2]

In [44]:
lista[::2]  # del primer elemento al último "de a 2"


Out[44]:
[5, 3, 1]

In [45]:
lista[::-1]  # del primer elemento al último "de a -1", invierte el orden!


Out[45]:
[1, 2, 3, 4, 5]

In [46]:
lista[:]  # un caso trivial, del primer al último elemento (equivale a no usar un slice!)


Out[46]:
[5, 4, 3, 2, 1]

Ya estamos un poco más cerca de calcular la mediana, pero nos falta aprender lo que se conoce como control de flujo. Es común que al escribir un programa necesitemos ejecutar una acción de forma condicional, algo del estilo si pasa A entonces X y si no B entonces Y. Esto se consigue en Python con el bloque if-else.


In [47]:
if 2 > 1: # esto es cierto
    print('hola')
else:
    print('chau')


hola

Si la condición a la derecha de if evalua como verdadero se ejecuta el cuerpo dentro de if, de lo contrario se ejecuta el bloque else. Veamos el siguiente ejemplo:


In [48]:
if 1 > 2:  # esto es falso
    print('hola')
else:
    print('chau')


chau

El bloque else es opcional


In [49]:
if True:  # esto es siempre cierto
    print('hola')


hola

Es posible revisar más de una condición, en el siguiente ejemplo debido a que la expresión a la derecha de if evalua como falso se evalua la siguiente condición, que en este caso evalua como verdadera.


In [50]:
if 1 > 2:  # esto es falso
    print('hola')
elif 2 > 1:  # esto es cierto, solo se ejecuta si if evalua falso
    print('hello')
else:
    print('chau')


hello

El bloque if-else es algo similar al bloque try-except, pero este segundo bloque está restringido a manejar posibles errores, mientras que el bloque if-else sirve para tomar decisiones que no tiene que ver con errores (o excepciones) si no con el funcionamiento normal de un programa.

Las listas vacías y otros contenedores vacíos (que veremos luego), el número 0, evalúan como False. Listas (u otros contenedores) con elementos, y números distintos de 0 evalúan como True.


In [51]:
if []:  # esto es falso
    print('hola')

Todavía faltan un par de detalles, pero los vamos a ver directamente en la función. Es común al aprender una lenguaje encontrarse con código que contiene elementos desconocidos o "frases" que no sabíamos que eran posibles o legales.


In [52]:
def mediana(lista):
    """
    Ejercicio escribir docstring!
    """
    lista_ordenada = sorted(lista)
    lista_len = len(lista_ordenada)
    idx = int((lista_len - 1) / 2)

    if lista_len % 2:
        return lista_ordenada[idx]
    else:
        return (lista_ordenada[idx] + lista_ordenada[idx + 1]) / 2

Esta es una función bastante más compleja que calcular_media(). Veamos línea por línea qué es lo que hace.

  1. La primer línea (luego del docstring) ordena de menor a mayor los elementos dentro de "lista".
  2. La segunda computa la longitud de lista.
  3. La tercera calcula un índice, usamos int para asegurarnos que el resultado es un entero.
  4. Esta línea evalúa True solo si lista_len es impar ¿Podés darte cuenta por qué? (tip: probá ese bloque de código en una celda separada).
  5. Si lista_len es par entonces se ejecutará el bloque else. La razón de tener este bloque es que si la cantidad de elementos es par NO es posible obtener el valor "del medio", entonces la mediana la computamos como un promedio de los dos valores "del medio".

In [53]:
mediana([1, 2, 3, 4, 5])


Out[53]:
3

Además de la media y la mediana existen otras medidas de centralidad como la moda. La moda es simplemente el valor más frecuente en un conjunto de datos.

Varianza

Mide la dispersión de un conjunto de valores. Es cero para un conjunto de valores idénticos.

$$V(\mathbf{x}) = E[(\mathbf{x} - \mu)^2] = \frac{1}{n} \sum_{i=1}^n (\mu - x_i)^2 \tag{1}$$

Donde $\mu$ es la media de $\mathbf{x}$,

Al igual que pasa con la media, se suele distinguir entre la varianza de la población $\sigma^2$ y la varianza de una muestra $s^2$. Pero a diferencia de la media se puede probar que la fórmula anterior para $s^2$ subestima el valor de $\sigma^2$. Es posible obtener un mejor estimador con una ligera modificación de la ecuación 1, la cual consiste en dividir por $n-1$ en vez de $n$. En la práctica esta corrección solo es importante para cuando se tienen pocos datos, a medida que los datos aumentan no hay mucha diferencia entre usar una u otra fórmula. Esto es facil de ver intuitivamente, para $n=2$ la diferencia entre $n$ y $n-1$ es relativamente grande, mientras que para $n=100$ esta diferencia es mucho más pequeña.

A partir de la definición de varianza, vemos que para calcularla debemos repetir la operación $(\mu - x_i)^2$ para cada valor en $\mathbf{x}$ es decir debemos iterar, esto es una operación muy común en programación. Una de las formas más comunes de iteración es mediante el bucle for, por ejemplo para iterar sobre todos los valores contenidos en la lista num hacemos:


In [54]:
for i in num:
    print(i)


1
2
3
4
5
6

Una operación muy común es querer iterar sobre una lista de valores, modificarlos y guardarlos en otra lista, por ejemplo:


In [55]:
num_2 = []
for i in num:
    num_2.append(i**2)
    #print(num_2)
num_2


Out[55]:
[1, 4, 9, 16, 25, 36]
  1. En la primer línea definimos una lista vacía.
  2. Luego iteramos sobre num, en cada iteración i toma un valor distinto.
  3. Calculamos el cuadrado de i y adjuntamos ese valor a la lista num_2.

Es importante notar que append agrega elementos al final de la lista, por eso el cuadrado de 6 ( el último elemento de num) es el último elemento de num_2. Para ver append en acción descomentá la linea 4 de la celda anterior.

Ahora si ya estamos en condiciones de calcular la varianza.


In [56]:
def varianza(valores):
    """
    ejercicio escribir docstring!
    """
    media = calcular_media2(valores)                                                     
    var = []
    for i in valores:
        var.append((media - i) ** 2)
    return calcular_media2(var)

In [57]:
varianza([0, 1, 2.72, 3.14])


Out[57]:
1.623275

¿Notaron que la función varianza usa la función calcular_media2? No hay ningún problema en que una función llame a otra función (incluso existen funciones que se llaman a si mismas!). Esto permite escribir funciones complejas a partir de otras funciones más simples.

No solo es posible iterar sobre listas, se puede iterar sobre otros objetos, la única condición es que sean iterables. Otros objetos iterables son las cadenas. Por ejemplo:


In [58]:
for c in 'hola':
    print(c)


h
o
l
a

El siguiente patrón, donde iteramos a la largo de una lista de enteros es tan común en Python (y otros lenguajes) que existe varias funciones que facilitan esta tarea. Una de ellas es range:


In [59]:
d = []
for i in [0, 1, 2, 3, 4, 5, 6, 7, 8]:
    d.append(i ** 2)
d


Out[59]:
[0, 1, 4, 9, 16, 25, 36, 49, 64]

range usa la sintaxis [start,stop,step] a fin de generar enteros desde start, hasta stop (sin incluirlo) y opcionalente de a step pasos (por defecto 1). start es también opcional en cuyo caso empezará en 0. Como pueden ver la sintaxis es similar a lo que ya vimos con la rebanadas de una lista. La diferencia es que las rebanadas operan sobre una lista existente y la función de range es la de generar un objeto que contiene enteros.


In [60]:
d = []
for i in range(0, 9):
    d.append(i ** 2)
d


Out[60]:
[0, 1, 4, 9, 16, 25, 36, 49, 64]

En Python3 range es un objeto que contiene las reglas para devolver valores, pero no los valores en si. Esto es un truco que permite ocupar menos memoria. Intuitivamente se puede ver que se requiere menos memoria para especificar la regla, "devuelva todos los enteros de 0 a 1 millón", que para escribir un millón de enteros.

En el siguiente ejemplo vemos una diferencia entre el objeto range y una lista generada a partir de convertir range usando el comando list.


In [61]:
range(9), list(range(9))


Out[61]:
(range(0, 9), [0, 1, 2, 3, 4, 5, 6, 7, 8])

Como veníamos diciendo al trabajar con Python es común repetir una frase como la siguiente:

  • Creamos una lista vacía.
  • Iteramos a largo de algún iterable como una lista o un rango.
  • Guardamos valores en la lista originalmente vacía.

Este patrón es tan común que Python ofrece una versión alternativa, la cual es considerada por la mayoría de Pitonistas como más simple y clara, aunque al principio puede que no se vea como simple y clara :-(

Esta versión alternativa se llama list comprehension, o comprensión por listas. Y luce de la siguiente forma:


In [62]:
d = [i ** 2 for i in range(9)]
d


Out[62]:
[0, 1, 4, 9, 16, 25, 36, 49, 64]

En palabras podríamos leerla como, "genere una lista tomando la variable i elevándola al cuadrado y repitiendo esto para todos los valores de i en el rango de 0 a 9".

Usando list compreherions podemos calcular la varianza de la siguiente forma:


In [63]:
def varianza(valores):
    """
    ejercicio escribir docstring!
    """
    media = calcular_media2(valores)
    var = [(media - i) ** 2 for i in valores]
    return calcular_media2(var)

Desviación estándar

La desviación estándar es la raíz cuadrada de la varianza, en muchos problemas teóricos resulta más fácil manipular varianzas que desviaciones estándar, pero en general resulta más simple interpretar las desviaciones estándar ya que las unidades son las mismas que las de los datos.

$$\sigma = \sqrt{V(x)}$$

Diccionarios

Los diccionarios son similares a las listas, una de las diferencias es que para indexar una lista solo es posible usar enteros, en cambio para indexar un diccionario podemos usar otros tipos.

Un diccionario es una forma de mapear un conjuntos de índices (llamados claves o keys) y un conjunto de valores. A cada par clave-valor (key-value) se le suele llamar item.

Podemos crear un diccionario vacio de la siguiente forma:


In [64]:
diccionario = {}  # o también diccionario = dict()
type(diccionario)


Out[64]:
dict

Y luego podemos agregar elementos especificando la clave entre [] y asignando un valor mediante el signo =


In [65]:
diccionario['Santiago'] = 'Maldonado'

diccionario


Out[65]:
{'Santiago': 'Maldonado'}

Al igual que con las listas, no es necesario crear un diccionario vacío y luego agregar valores, podríamos haber creado el diccionario de forma directa. En el siguiente ejemplo tenemos los nombres de 5 ciudades y sus poblaciones.


In [66]:
pob = {'BSAS':15594428,
       'Córdoba':3304825,
       'Santa Fe':3300736,
       'CABA':2891082,
       'Mendoza':1741610}

Si ahora quisiramos saber la población de cordoba, bastaría con escribir:


In [67]:
pob['Córdoba']


Out[67]:
3304825

Una operación que suele ser útil es preguntar si una clave existe en un diccionario, esto se logra con el operador in:


In [68]:
'San Luis' in pob


Out[68]:
False

El operador in funciona tanto con listas como con diccionarios, aunque internamente no funciona de igual forma para ambas estructuras de datos. En el caso de las listas el tiempo de demora de esta operación es proporcional a la longitud de la lista, mientras más elementos estén contenidos en la lista más tardaremos en obtener una respuesta. Por el contrario el tiempo de demora es casi constante para los diccionarios. Quienes tengan mayor curiosidad sobre como se logra esta característica pueden leer sobre Hash table.

Si quisieramos saber si un valor está contenido en un diccionario deberíamos hacer:


In [69]:
3304825 in pob.values()


Out[69]:
True

Así como iteramos sobre los valores contenidos en un lista es posible iterar sobre los valores contenidos en un diccionario:


In [70]:
for k, v in pob.items():
    print(k, v)


BSAS 15594428
Córdoba 3304825
Santa Fe 3300736
CABA 2891082
Mendoza 1741610

Es común usar estadísticos como la media y la desviación estándar para resumir (o comprimir) un conjunto de datos. Esto proceso de compresión de información puede conducir a la pérdida de información útil, por lo que suele ser buena idea usar otras formas de representar datos, una muy común es mediante histogramas. Un histograma es una representación visual que muestra el número de veces que aparece un número es un conjunto de datos (frecuencia). En el próximo capítulo veremos como hacer este tipo de gráficos. Pero antes de llegar a graficar veamos como podemos usar un diccionario para calcular estas frecuencias. Supongamos que tenemos una lista de valores como la siguiente


In [71]:
lista = [3, 5, 5, 2, 2, 4, 1]

Y queremos saber cuantas veces aparece cada número en esa lista, el 1 una vez, el 2 dos veces el 3 una vez, etc. Usando un diccionario podemos realizar esta tarea de forma eficiente y relativamente sencilla.


In [72]:
def frecuencias(datos):
    """
    """
    frec = {}
    for c in datos:
        if c not in frec:
            frec[c] = 1
        else:
            frec[c] += 1
    return frec

In [73]:
lista = [1, 2, 2, 3, 4, 5]
frecuencias(lista)


Out[73]:
{1: 1, 2: 2, 3: 1, 4: 1, 5: 1}

Tuplas

Las tuplas son similares a las listas, podemos usarla para almacenar otros objetos


In [74]:
unatupla = (42, 'a', [3])

unatupla


Out[74]:
(42, 'a', [3])

Podemos indexarlas


In [75]:
unatupla[1]


Out[75]:
'a'

Y podemos tomar rebanadas


In [76]:
unatupla[1:]


Out[76]:
('a', [3])

Una diferencia con las listas es que las tuplas son inmutables, es decir no pueden ser modificadas una vez creadas


In [77]:
unatupla[0] = 0


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-77-7caa37e7ce07> in <module>()
----> 1 unatupla[0] = 0

TypeError: 'tuple' object does not support item assignment

Dado que las tuplas y las listas parecidas es común que surga la pregunta ¿Cúando es conveniente usar una y cuando la otra?

Al dar los primeros pasos con Python esta elección no es demasiado relevante, ya que en muchos casos el uso es más o menos indistinto. De todas formas es bueno saber que uno de los criterios usados para elegir entre listas y tuplas tiene que ver con el tipo de elementos que vamos a almacenar. Si bien tanto las tuplas como las listas pueden contener elementos de distinto tipo (por ej strings e int). Es más común que se use listas para almacenar elementos del mismo tipo y tuplas para elementos de distinto tipo.

Operaciones sobre strings

En Python es posible iterar, rebanar e indexar strings como si fueran listas


In [78]:
s = 'ciencia'

In [79]:
s[0]


Out[79]:
'c'

In [80]:
s[::-1]


Out[80]:
'aicneic'

También es posible realziar otro tipo de operaciones que normalmente consideramos definidaas solo para números, es posible sumar strings


In [81]:
s + ' de ' + 'datos'


Out[81]:
'ciencia de datos'

y multiplicarlos


In [82]:
(s + ' ') * 5


Out[82]:
'ciencia ciencia ciencia ciencia ciencia '

Python ofrece muchos métodos para operar sobre strings.


In [83]:
s.capitalize()


Out[83]:
'Ciencia'

In [84]:
s.count('c')


Out[84]:
2

Para escribir un string podemos usar comillas simples o dobles, cual usar es una cuestión de preferencia.


In [85]:
'simples'


Out[85]:
'simples'

In [86]:
"dobles"


Out[86]:
'dobles'

También es posible usar comillas triples (triples-simples o triples-dobles)


In [87]:
'''tri-
ples'''


Out[87]:
'tri-\nples'

La ventaja de las comilas triples es que permite escribir un string que ocupe varias lineas (como ya vimos con los docstrings). \n se usa para indicar que el string contiene una nueva linea de texto


In [88]:
print('''tri-
ples''')


tri-
ples

In [89]:
print('tri-\nples')


tri-
ples

Bibliotecas

En los ejemplos anteriores hemos visto como calcular la media, mediana y la varianza. Vimos además que es posible encapsular código dentro de funciones y así reutilizarlo. El principal motivo de estos ejemplos fue motivar el aprendizaje de Python. En la práctica el cálculo de las funciones como la media o la varianza son tan comunes que resulta muy conveniente poder acceder a estas funciones sin necesidad de que debamos escribirlas nosotros. Es por esto que en Python (y otros lenguajes) existen las bibliotecas, que permiten extender el lenguaje. En el fondo estás bibliotecas (llamadas también librerías, por una transliteración del ingles library) no son otra cosa que un conjunto de funciones del estilo que aprendimos a escribir en este capítulo. En general las bibliotecas contienen funciones optimizadas, es decir funciones que corren rápido o que reducen la posibilidad de errores numéricos. Por lo que la ganancia al usarlas no es solo que nos ahorran tiempo si no que en general nos ahorran dolores de cabeza!

Python posee un extenso ecosistema de bibliotecas, muchas de ellas específicas para hacer ciencia. Algunas de esas bibliotecas más usadas son:

  • Numpy: Cálculo numerico y algebra lineal
  • Scipy: - Funciones comunmente usadas en ciencias
  • Matplotlib: - Gráficas científicas
  • Seaborn: - Gráficas cientificas atractivas.
  • Jupyter: - Computación interactiva
  • Pandas: - Procesamiento de datos
  • Scikit-learn: - Machine Learning
  • Statsmodels: Estadística "clásica"
  • PyMC3: - Estadística Bayesiana
  • SymPy - Matemática simbólica
  • Sage - Es un entorno matemática basado en Python y varias de las bibliotecas arriba mencionadas
  • agregá tu paquete favorito

Incluso algunas bibliotecas se construyen sobre otras bibliotecas. En el siguiente esquema se ve parte del ecosistema de Python ordenado (radialmente). Donde un nivel se apoya sobre los inferiores como en un cebolla.

Además de estas bibliotecas externas Python se distribuye con algunas bibliotecas estándar, por ejemplo math tiene varias funciones matemáticas. Para poder usarla necesitamos importarla de la siguiente forma.


In [90]:
import math

Ahora que la importamos podemos usarla por ejemplo para calcular un logaritmo.


In [91]:
math.log(10)


Out[91]:
2.302585092994046

Qué otras funciones están disponibles dentro de math? Una forma de averiguarlo es usando la ayuda que nos ofrece jupyter. Recordá que basta escribir math. seguido de Tab para que Jupyter nos haga recomendaciones!

Existen varias formas de importar modulos en Python, una alternativa a lo ya visto es importar una función específica, por ejemplo sqrt.


In [92]:
from math import sqrt

In [93]:
sqrt(4)


Out[93]:
2.0

También podemos importar una función pero cambiandole el nombre


In [94]:
from math import sqrt as raiz

In [95]:
raiz(4)


Out[95]:
2.0

Ejercicios

  1. ¿Cual es el resultado de sumar los elementos de una lista que contiene strings? Explique
  2. ¿Cual es el resultado de sumar los elementos de una lista que contiene solo booleanos? Explique
  3. Dentro de Jupyter se puede acceder a la ayuda escribiendo el nombre de un comando seguido de ? o ?? .¿Cual es la diferencia entre usar una u otra forma?
  4. En un lista que contiene 5 elementos, que elementos serán devueltos al usar los índices 1, -2, -6
  5. Ejecutar en una celda:

    import this

    Este texto es conocido como el Zen de Python. Es común que al escribir código pensemos en más de una forma de escribir lo mismo. Dentro de la comunidad de Python se suele dar preferencia a una de estas formas sobre las otras, debido a que es más simple de comprender, menos propensa a errores, más eficiente, más comunmente usada, etc. Un código que satisface estos criterios, mejor que otro, se dice que es Pytónico.

  6. ¿Cúal es la diferencia entre muestra y población?
  7. Comparar la media y mediana para algunos conjuntos de datos usando las funciones antes implementadas.
  8. Escribir una función para calcular la media truncada. Es decir el valor de la media luego de haber eliminado una cantidad determinada (por ej el 5%) de valores a ambos extremos del conjunto de datos.
  9. Escribir una función para calcular la moda de un conjunto de valores discretos. La función frecuencias te puede servir.
  10. ¿Cuán facil sería generalizar el computo de la moda para números reales?
  11. Modificar la función que computa la varianza agregando un nuevo argumento, llamado "ddof" (delta degree of freedom) de forma tal que cuando ddof=1 la función use $n-1$ y cuando ddof=0 use $n$.
  12. Una forma de estimar la dispersión de los datos es usando el rango. El rango es la diferencia entre el valor máximo y el valor mínimo de un conjunto de datos. Escribir una función para computar el rango.
  13. ¿Cuán robusto es el rango comparado con la varianza? Justificar mostrando un ejemplo.

Para seguir leyendo

Python es un lenguaje muy rico y expresivo que cuenta con muchas otras características útiles las cuales hemos omitido ya que no serán necesarias para el resto del curso o que podrán ser introducidas a medida que sea necesario.

A continuación listamos algunas de estas características:

  • Escritura y lectura de archivos
  • Sets
  • Clases y programación orientada a objetos
  • Generadores
  • Decoradores

Hay mucho material disponible para quienes tengan interés en aprender estos tópicos, nosotros proponemos los siguientes libros, en orden de complejidad/profundidad