Imagine que Ud. es un trabaja en una compañía de seguros para la cual es necesario evaluar constantemente el nivel de riesgo de un cliente en base a sus antecedentes antes de negociar un producto. ¿Será posible automatizar el proceso con el fin de trabajar menos, mejorar los tiempos de evaluación y hacer el proceso más eficiente?
La respuesta a esta y muchas otras preguntas se encuentra en la creación de algoritmos de programación.
Instrucciones de instalación y utilización de un ipython notebook.
Recuerden:
Ctr-S
para evitar sorpresas.FIX_ME
por el código correspondiente.Ctr-Enter
Ejecutar la siguiente celda mediante Ctr-Enter
.
In [1]:
"""
IPython Notebook v4.0 para python 3.0
Librerías adicionales:
Contenido bajo licencia CC-BY 4.0. Código bajo licencia MIT.
(c) Sebastian Flores, Christopher Cooper, Alberto Rubio, Pablo Bunout.
"""
# Configuración para recargar módulos y librerías dinámicamente
%reload_ext autoreload
%autoreload 2
# Configuración para graficos en línea
%matplotlib inline
# Configuración de estilo
from IPython.core.display import HTML
HTML(open("./style/style.css", "r").read())
Out[1]:
Entenderemos por algoritmo a una serie de pasos que persiguen un objetivo específico. Intuitivamente lo podemos relacionar con una receta de cocina: una serie de pasos bien definidos (sin dejar espacio para la confusión del usuario) que deben ser realizados en un orden específico para obtener un determinado resultado. En general un buen algoritmo debe poseer las siguientes características:
Por otra parte, llamaremos código a la materialización, en base a la implementación en la sintaxis adecuada de un determinado lenguaje de programación, de un determinado algoritmo. Entonces, para escribir un buen código y que sea eficiente debe tratar de respetar las ideas anteriores: se debe desarrollar en un número finito de pasos, ocupar adecuadamente las estructuras propias del lenguaje, se debe poder ingresar y manipular adecuadamente los datos de entrada y finalmente entregar el resultado deseado.
A diferencia de lo anterior, una idea un poco menos estructurada es el concepto de pseudo-código. Entenderemos por el anterior a la descripción informal de un determinado algoritmo en un determinado lenguaje de programación. Sin embargo, no debe perder las características esenciales de un algoritmo como claridad en los pasos, inputs y outputs bien definidos, etc. de tal forma que permita la implementación directa de éste en el computador.
Una vez implementado un algoritmo viene el proceso de revisión de éste. Para realizar adecuadamente lo anterior se recomienda contestar las siguentes preguntas:
A continuación estudiamos el problema de determinar si un número entero $N\geq 2$ es primo o no. Consideremos los siguientes números: 8191 (primo), 8192 (compuesto), 49979687 (primo), 49979689 (compuesto).
Nuestro primer algoritmo para determinar si un numero es primo es: verificar que ningún número entre $2$ y $N-1$ sea divisor de $N$.
El pseudo-código es:
El código es el siguiente:
In [22]:
N = int(raw_input("Ingrese el numero que desea estudiar "))
if N<=1:
print("Numero N tiene que ser mayor o igual a 2")
elif 2<=N<=3:
print("{0} es primo".format(N))
else:
es_primo = True
for i in range(2, N):
if N%i==0:
es_primo = False
if es_primo:
print("{0} es primo".format(N))
else:
print("{0} es compuesto".format(N))
Al utilizar números grandes ($N=10^7$, por ejemplo) nos damos cuenta que el algoritmo anterior tarda mucho tiempo en ejecutar, y que recorre todos los numeros. Sin embargo, si se encuentra un divisor ya sabemos que el número no es primo, pudiendo detener inmediatamente el algoritmo. Esto se consigue utilizando únicamente una línea extra, con una sentencia break
.
El algoritmo para verificar si un numero no primo es: verificar si algún numero entre $2$ y $N-1$ es divisor de $N$.
El pseudo-código es:
Mientras que el código es el siguiente:
In [23]:
N = int(raw_input("Ingrese el numero que desea estudiar "))
if N<=1:
print("Numero N tiene que ser mayor o igual a 2")
elif 2<=N<=3:
print("{0} es primo".format(N))
else:
es_primo = True
for i in range(2, N):
if N%i==0:
es_primo = False
break
if es_primo:
print("{0} es primo".format(N))
else:
print("{0} es compuesto".format(N))
La ejecución de números grandes compuestos se detiene en el primer múltiplo cuando el número es compuesto. Sin embargo, para numeros grandes y primos tarda bastante.
Un último truco que podemos utilizar para verificar más rápidamente si un número es primo es verificar únicamente parte del rango de los múltiplos. Esto se explica mejor con un ejemplo. Consideremos el número 16: los multiplos son 2, 4 y 8. Como el número es compuesto, nuestro algoritmo anterior detecta rápidamente que 2 es un factor, detiene el algoritmo e indica que el número 12 no es primo. Consideremos ahora el numero 17: es un número primo y no tiene factores, por lo que el algoritmo revisa los numeros 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 y 16. Sin embargo, sólo es necesario revisar los números 2, 3, 4, 5 y 6 porque para que exista un factor mayor a 6, tiene que simultáneamente haber un factor menor a 6 tal que la multiplicación sea 17. Esto es, basta revisar los factores más pequeños, donde la cota está dada por el entero más cercano a $\sqrt{17}$ o en general, $\sqrt{N}$.
El algoritmo para verificar si un numero no primo es: verificar si algún numero entero entre $2$ y $\sqrt{N}$ es divisor de $N$.
El pseudo-código es:
Mientras que el código es el siguiente:
In [24]:
N = int(raw_input("Ingrese el numero que desea estudiar "))
if N<=1:
print("Numero N tiene que ser mayor o igual a 2")
elif 2<=N<=3:
print("{0} es primo".format(N))
else:
es_primo = True
for i in range(2, int(N**.5)):
if N%i==0:
es_primo = False
break
if es_primo:
print("{0} es primo".format(N))
else:
print("{0} no es primo".format(N))
Como dijimos anteriormente luego de hacer que un algoritmo funcione, una de las preguntas más importantes es la revisión de éste haciendo énfasis en la medición del tiempo que necesita para resolver el problema. Así, la primera interrogante es: ¿cómo podemos medir el tiempo que tarda un algoritmo en relación al tamaño del problema que resuelve? Esto se denomina usualmente como complejidad temporal o, en inglés, como time complexity o escalability.
Sin embargo es importante notar que medir la complejidad temporal de un algoritmo puede resultar un poco complejo puesto que: (a) El tiempo que toma al computador realizar las distintas operaciones en general es heterogeneo, es decir, realizar una suma es mucho más rápido que hacer una división, (b) Computadores distintos puede realizar un determinado experimento en tiempos distintos.
La notación estándar para la complejidad de un algoritmo es mediante la letra mayúscula O, por lo que la complejidad de alguna función la podemos expresar por O("función"), lo que podemos interpretar como que el número de operaciones es proporcional a la función por una determinada constante. Las complejidades más importantes son:
En nuestros algoritmos anteriormente desarrollados:
Cuando un algoritmo se utiliza muy seguido, resulta conveniente encapsular su utilización en una función. Ahora bien, resulta importante destacar que en informática una función no tiene el mismo significado que en matemáticas. Una función (en python) es simplemente una sucesión de acciones que se ejecutan sobre un conjunto de variables de entrada para producir un conjunto de variables de salida.
La definición de funciones se realiza de la siguiente forma:
def nombre_de_funcion(variable_1, variable_2, variable_opcional_1=valor_por_defecto_1, ...):
accion_1
accion_2
return valor_1, valor_2
A continuación algunos ejemplos.
In [4]:
def sin_inputs_ni_outputs():
print "Hola mundo"
def sin_inputs():
return "42"
def sin_outputs(a,b):
print a
print b
def con_input_y_output(a,b):
return a+b
def con_tuti(a,b,c=2):
return a+b*c
La función sin_inputs_ni_outputs
se ejecuta sin recibir datos de entrada ni producir datos de salida (Y no es muy útil).
In [2]:
sin_inputs_ni_outputs()
La función sin_inputs
se ejecuta sin recibir datos de entrada pero si produce datos de salida.
In [6]:
x = sin_inputs()
print("El sentido de la vida, el universo y todo lo demás es: "+x)
La función con_input_y_output
se ejecuta con datos de entrada y produce datos de salida. Cabe destacar que como python no utiliza tipos de datos, la misma función puede aplicarse a distintos tipos de datos mientras la lógica aplicada dentro de la función tenga sentido (y no arroje errores).
In [7]:
print con_input_y_output("uno","dos")
print con_input_y_output(1,2)
print con_input_y_output(1.0, 2)
print con_input_y_output(1.0, 2.0)
La función con_tuti
se ejecuta con datos de entrada y valores por defecto, y produce datos de salida.
In [11]:
print con_tuti(1,2)
print con_tuti("uno","dos")
print con_tuti(1,2,c=3)
print con_tuti(1,2,3)
print con_tuti("uno","dos",3)