100% encontró este documento útil (1 voto)
598 vistas

Introduction To Computation and Programming Using Python, Revised - Guttag, John V..en - Es

Este documento presenta una introducción a la computación y programación usando Python. Explica conceptos básicos como objetos, expresiones, tipos numéricos, variables, iteración y derivación de programas. También incluye ejemplos de programas numéricos simples y explica funciones, alcance, recursión, módulos, archivos, tipos estructurados, pruebas, depuración, excepciones, programación orientada a objetos y una introducción a la complejidad algorítmica.

Cargado por

Sara uwu
Derechos de autor
© © All Rights Reserved
Formatos disponibles
Descarga como DOCX, PDF, TXT o lee en línea desde Scribd
100% encontró este documento útil (1 voto)
598 vistas

Introduction To Computation and Programming Using Python, Revised - Guttag, John V..en - Es

Este documento presenta una introducción a la computación y programación usando Python. Explica conceptos básicos como objetos, expresiones, tipos numéricos, variables, iteración y derivación de programas. También incluye ejemplos de programas numéricos simples y explica funciones, alcance, recursión, módulos, archivos, tipos estructurados, pruebas, depuración, excepciones, programación orientada a objetos y una introducción a la complejidad algorítmica.

Cargado por

Sara uwu
Derechos de autor
© © All Rights Reserved
Formatos disponibles
Descarga como DOCX, PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 337

Traducido del inglés al español - www.onlinedoctranslator.

com
Introducció n a la
computació n y
programació n usando
Python
Introducció n a la
computació n y
programació n usando
Python

Edició n revisada y ampliada

Juan V. Guttag

La prensa del MIT


Cambridge, Massachusetts
Londres, Inglaterra
© 2013 Instituto de Tecnología de Massachusetts

Reservados todos los derechos. Ninguna parte de este libro puede ser reproducida
en forma alguna por ningú nmedios electró nicos o mecá nicos (incluidas fotocopias,
grabaciones o almacenamiento y recuperació n de informació n) sin el permiso por
escrito del editor.

Los libros de MIT Press se pueden comprar con descuentos especiales por cantidad
para uso comercial o de promoció n de ventas. Para obtener informació n, envíe un
correo electró [email protected] o escriba al Departamento de
Ventas Especiales, The MIT Press, 55 Hayward Street, Cambridge, MA 02142.

Impreso y encuadernado en los Estados Unidos de América.

bibliotecarioyoFCongresosCatalogació ng-‐en-‐

Publicació ndataGutag, John.

Introducció n a la computació n y programació n usando Python / John V. Guttag. —


Edició n revisada y ampliada.

paginascm

Incluye índice.

ES Bnorte978-‐0-‐262-‐52500-‐8(pbk. :alkpapel)

1. Python (lenguaje de programació n de computadoras) 2. Programació n de

computadoras. I. Título.QA76.73.P48G882013

005.13'3—dc23

10987654321
A mi familia:

Olga
David
Andrea
Michae
l Mark
Addie
CONTENIDO

PREFACIOxiii...............................................................................................................................................

AGRADECIMIENTOSxv...........................................................................................................................

1 CONSEGUIRINICIADO1...................................................................................................................

2 INTRODUCCIÓ N APITÓ N7....................................................................................................................

2.1 Los Elementos Bá sicos dePython8..........................................................................................

2.1.1 Objetos, Expresiones y Numé ricoTipos9.............................................................

2.1.2 Variables yTarea11........................................................................................................

2.1.3 INACTIVO13.......................................................................................................................

2.2 Derivació nProgramas14..............................................................................................................

2.3 Cuerdas yEntrada16...............................................................................................................

2.3.1 Entrada18..................................................................................................................................

2.4 Iteració n18.........................................................................................................................................

3 ALGUNOS NUMÉ RICOS SIMPLESPROGRAMAS21.............................................................

3.1 ExhaustivoEnumeració n21..................................................................................................

3.2 ParaBucles23.....................................................................................................................................

3.3 Soluciones aproximadas y bisecció nBuscar25...........................................................

3.4 Algunas palabras sobre el usoFlotadores29.......................................................................

3.5 Newton-Raphson32.......................................................................................................................

4 FUNCIONES, ALCANCE yABSTRACCIÓ N34...........................................................................

4.1 Funciones yAlcance35............................................................................................................

4.1.1 Funció nDefiniciones35........................................................................................................

4.1.2 Argumentos de palabras clave y predeterminadosValores36..........................

4.1.3 Alcance37............................................................................................................................

4.2 Especificaciones41..........................................................................................................................
4.3 Recursió n44.......................................................................................................................................

4.3.1 FibonacciNú meros45...........................................................................................................

4.3.2 palíndromos48........................................................................................................................

4.4 GlobalVariables50....................................................................................................................

4.5 Mó dulos51..........................................................................................................................................

4.6 Archivos53..........................................................................................................................................

5 TIPOS ESTRUCTURADOS, MUTABILIDAD Y FUNCIONES DE ORDEN

SUPERIOR.. 56 5.1Tuplas56.......................................................................................

5.1.1 Secuencias y MúltiplesAsignación.......................................................57

5.2 Listas ymutabilidad58.................................................................................

5.2.1 Clonación63..........................................................................................

5.2.2 ListaComprensión63............................................................................

5.3 Funciona comoObjetos64............................................................................

5.4 Cadenas, tuplas yListas66...........................................................................

5.5 Diccionarios67.............................................................................................

6 PRUEBAS YDEPURACIÓN70................................................................................

6.1 Prueba70......................................................................................................

6.1.1 Caja negraPruebas................................................................................71

6.1.2 Caja de vidrioPrueba73.........................................................................

6.1.3 ConductiblePruebas74..........................................................................

6.2 Depuración76...............................................................................................

6.2.1 Aprendiendo adepuración78................................................................

6.2.2 Diseñando elExperimento79................................................................

6.2.3 Cuando las cosas se ponenDuro81.......................................................

6.2.4 Y cuando hayas encontrado “El”error82..................................................

7 EXCEPCIONES YASERCIONES84........................................................................

7.1 ManejoExcepciones84......................................................................................

7.2 Excepciones como flujo de controlMecanismo87......................................

7.3 Afirmaciones90................................................................................................

8 CLASES Y ORIENTACIÓN A OBJETOSPROGRAMACIÓN.................................91


8.1 Tipos de datos abstractos yClases91............................................................

8.1.1 Diseño de programas utilizando datos abstractosTipos96.................

8.1.2 Uso de clases para realizar un seguimiento de los estudiantes yfacultad96

8.2 herencia99....................................................................................................

8.2.1 Múltiples niveles deherencia101........................................................

8.2.2 la sustituciónPrincipio 102.................................................................

8.3 Encapsulación e InformaciónOcultar103.................................................

8.3.1 Generadores106..................................................................................

8.4 Hipotecas, una extensiónEjemplo108......................................................

9 UNA INTRODUCCIÓ N SIMPLISTA A LA ALGORITMICACOMPLEJIDAD113...........

9.1 Pensando en ComputacionalComplejidad113...................................................................

9.2 Asintó ticoNotació n116.................................................................................................................

9.3 Alguna Complejidad Importanteclases118.........................................................................

9.3.1 ConstanteComplejidad118..........................................................................................

9.3.2 logarítmicoComplejidad118.............................................................................................

9.3.3 LinealComplejidad119........................................................................................................

9.3.4 Log-LinealComplejidad120...............................................................................................

9.3.5 PolinomioComplejidad120................................................................................................

9.3.6 ExponencialComplejidad121............................................................................................

9.3.7 Comparaciones de ComplejidadClases123..........................................................

10 ALGUNOS ALGORITMOS Y DATOS SENCILLOSESTRUCTURAS125..........................


10.1 BuscarAlgoritmos126.................................................................................................................
10.1.1 Bú squeda lineal y uso de la indirecció n para accederElementos126.........

10.1.2 Bú squeda binaria y explotació nSupuestos128...............................................

10.2 Clasificació nAlgoritmos131.....................................................................................................


10.2.1 Unirordenar132...................................................................................................................

10.2.2 Explotando funciones comoPará metros135..........................................................

10.2.3 clasificando enpitó n136...................................................................................................

10.3 PicadilloMesas137................................................................................................................
11 PLOTEAR Y MAS SOBRECLASES141........................................................................................
11.1 Trazado usandoPyLab141........................................................................................................
11.2 Trazado de hipotecas, una extensió nEjemplo146..................................................
12 PROGRAMAS ESTOCÁ STICOS, PROBABILIDAD YESTADÍSTICAS152......................
12.1 estocá sticoProgramas153.................................................................................................
12.2 Estadística inferencial ySimulació n155......................................................................
12.3 Distribuciones166........................................................................................................................
12.3.1 Distribuciones normales y confianzaNiveles168.................................................

12.3.2 UniformeDistribuciones170..........................................................................................

12.3.3 Exponencial y GeométricaDistribuciones171.................................................

12.3.4 de BenfordDistribució n173............................................................................................

12.4 ¿Con qué frecuencia el mejor equipo¿Ganar?174.........................................................


12.5 hash yColisiones177.............................................................................................................

13 PASEOS ALEATORIOS Y MÁS SOBRE DATOSVISUALIZACIÓN179............................

13.1 el del borrachocaminar179............................................................................

13.2 Aleatorio sesgadoPaseos186..........................................................................

13.3 TraicioneroCampos191...........................................................................

14 MONTE CARLOSIMULACIÓN193.............................................................................

14.1 de pascalproblema194...................................................................................

14.2 Pase o no¿Pasar?195......................................................................................

14.3 Uso de la búsqueda de tablas para mejorarRendimiento199............................

14.4 Hallazgo.............................................................................................................200

14.5 Algunas observaciones finales sobre la simulaciónModelos204.................

15 COMPRENSIÓN EXPERIMENTALDATOS207.........................................................

15.1 el comportamiento demuelles207.................................................................

15.1.1 Uso de la regresión lineal para encontrar unajuste210.........................

15.2 el comportamiento deProyectiles214.................................................................

15.2.1 Coeficiente deDeterminación216...........................................................

15.2.2 Usando un ComputacionalModelo217...................................................


15.3 Ajuste distribuido exponencialmentedatos218............................................

15.4 Cuando la teoría esDesaparecido221............................................................

16 MENTIRAS, MALDITAS MENTIRAS YESTADÍSTICAS222.....................................

16.1 Basura dentro basura fuera(GIGO)222.............................................................

16.2 Las imágenes pueden serengañando223......................................................

16.3 Cum Hoc Ergo PropterHoc...............................................................................225

16.4 Las medidas estadísticas no dicen el todohistoria226.................................

16.5 Muestreosesgo228.........................................................................................

16.6 ContextoAsuntos229......................................................................................

16.7 Cuidado conExtrapolación229...........................................................................

16.8 El francotirador de TexasFalacia230............................................................

16.9 Los porcentajes puedenconfundir232...........................................................

16.10 JustoCuidado233.........................................................................................

17 OPTIMIZACIÓN DE GRÁFICOS Y MOCHILASPROBLEMAS234...........................

17.1 MochilaProblemas234...................................................................................

17.1.1 AvaroAlgoritmos235...............................................................................

17.1.2 Una solución óptima a la mochila 0/1problema238.............................

17.2 Optimizació n de grá ficosProblemas240............................................................................


17.2.1 Algo de teoría clá sica de grafosProblemas244...............................................

17.2.2 La propagació n de enfermedades y min.corte245........................................

17.2.3 Ruta má s corta: bú squeda primero en profundidad y primero en


amplitudBuscar246.....................................................................................................

18 DINÁ MICAPROGRAMACION252................................................................................................
18.1 secuencias de fibonacci,Revisado252..........................................................................
18.2 Programació n Diná mica y la Mochila 0/1problema254............................................
18.3 Programació n Diná mica yDivide y vencerá s261....................................................
19 UNA MIRADA RÁ PIDA A LA MÁ QUINAAPRENDIZAJE262............................................
19.1 CaracterísticaVectores264.......................................................................................................
19.2 DistanciaMétricas266.................................................................................................................
19.3 Agrupació n270.......................................................................................................................
19.4 Tipos Ejemplo yClú ster272...............................................................................................
19.5 K-mediasAgrupació n274...................................................................................................
19.6 un artificialEjemplo276.............................................................................................................
19.7 un menos artificialEjemplo280..............................................................................................
19.8 Envasearriba286...........................................................................................................................
PYTHON 2.7 RÁ PIDOREFERENCIA287....................................................................................

ÍNDICE289....................................................................................................................................................
PREFACIO

Este libro se basa en un curso del MIT que se ofrece dos veces al añ o desde 2006. El
curso está dirigido a estudiantes con poca o ninguna experiencia previa en
programació n que deseen comprender los enfoques computacionales para la
resolució n de problemas. Cada añ o, algunos de los estudiantes de la clase utilizan el
curso como un trampolín para cursos de informá tica má s avanzados. Pero para la
mayoría de los estudiantes será su ú nico curso de informá tica.

Debido a que el curso será el ú nico curso de informá tica para la mayoría de los
estudiantes, nos enfocamos en la amplitud en lugar de la profundidad. El objetivo es
brindarles a los estudiantes una breve introducció n a muchos temas, para que
tengan una idea de lo que es posible cuando llegue el momento de pensar en có mo
usar la computació n para lograr una meta. Dicho esto, no es un curso de
“apreciació n de la computació n”. Es un curso desafiante y riguroso en el que los
estudiantes dedican mucho tiempo y esfuerzo a aprender a manipular la
computadora a su antojo.

El objetivo principal de este libro es ayudarlo a usted, el lector, a ser há bil para hacer
un uso productivo de las técnicas computacionales. Debe aprender a aplicar modos
computacionales de pensamientos para enmarcar problemas y guiar el proceso de
extracció n de informació n de datos de manera computacional. El conocimiento
principal que obtendrá de este libro es el arte de la resolució n de problemas
computacionales.

El libro es un poco excé ntrico. La Parte 1 (Capítulos 1-8) es una introducció n


poco convencional a la programació n en Python. Trenzamos cuatro hilos de
material:

 Los fundamentos de la programació n,


 El lenguaje de programació n Python,
 Conceptos centrales para comprender la computació n, y
 Té cnicas computacionales de resolució n de problemas.
Cubrimos la mayoría de las funciones de Python, pero el é nfasis está en lo que se
puede hacer con un lenguaje de programació n, no en el lenguaje en sí. Por ejemplo, al
final del Capítulo 3, el libro ha cubierto solo una pequeñ a fracció n de Python, pero ya
ha introducido las nociones de enumeració n exhaustiva, algoritmos de adivinar y
verificar, bú squeda de bisecció n y algoritmos de aproximació n eficiente.
Presentamos características de Python a lo largo del libro. De manera similar,
presentamos aspectos de los mé todos de programació n a lo largo del libro. La idea es
ayudarlo a aprender Python y có mo ser un buen programador en el contexto del uso
de la computació n para resolver problemas interesantes.

La Parte 2 (Capítulos 9-16) trata principalmente sobre el uso de la computació n


para resolver problemas. No asume ningú n conocimiento de matemá ticas má s
allá del á lgebra de la escuela secundaria, pero sí asume que el lector se siente
có modo con el pensamiento riguroso y no se siente intimidado por los
conceptos matemá ticos. Cubre algunos de los temas habituales que se
encuentran en un texto introductorio, por ejemplo, complejidad computacional
y algoritmos simples.
xiv Prefacio

Pero la mayor parte de esta parte del libro está dedicada a temas que no se
encuentran en la mayoría de los textos introductorios: visualización de datos,
pensamiento probabilístico y estadístico, modelos de simulación y uso de
computación para comprender datos.

La Parte 3 (Capítulos 17-19) analiza tres temas ligeramente avanzados:


problemas de optimización, programación dinámica y agrupamiento.

La Parte 1 puede formar la base de un curso independiente que se puede


enseñar en un trimestre o medio semestre. La experiencia sugiere que es
bastante cómodo incluir las Partes 1 y 2 de este libro en un curso de un
semestre completo. Cuando se incluye el material de la Parte 3, el curso se
vuelve más exigente de lo que es cómodo para muchos estudiantes.

El libro tiene dos temas generalizados: la resolución sistemática de problemas y


el poder de la abstracción. Cuando hayas terminado este libro deberías tener:

 Aprendió un lenguaje, Python, para expresar cálculos,


 Aprendió un enfoque sistemático para organizar, escribir y depurar
programas de tamaño mediano,
 Desarrolló una comprensión informal de la complejidad computacional,
 Desarrolló una idea del proceso de pasar de un enunciado ambiguo del
problema a una formulación computacional de un método para resolver
el problema.
 Aprendió un conjunto útil de técnicas algorítmicas y de reducción de problemas,
 Aprendió a usar la aleatoriedad y las simulaciones para arrojar luz
sobre problemas que no sucumben fácilmente a soluciones de
forma cerrada, y
 Aprendió a usar herramientas computacionales, incluidas herramientas
estadísticas y de visualización simples, para modelar y comprender datos.
La programación es una actividad intrínsecamente difícil. Así como “no existe un
camino real hacia la geometría”,1 no existe un camino real hacia la programación.
Es posible engañar a los estudiantes para que piensen que han aprendido a
programar haciéndoles completar una serie de problemas de programación
altamente restringidos de "llenar espacios en blanco". Sin embargo, esto no
prepara a los estudiantes para descubrir cómo aprovechar el pensamiento
computacional para resolver problemas.

Si realmente quieres aprender el material, leer el libro no será suficiente. Como


mínimo, debería intentar ejecutar parte del código del libro. Todo el código del libro
se puede encontrar enhttp://mitpress.mit.edu/ICPPRE. Varias versiones del
curso han estado disponibles en el sitio web OpenCourseWare (OCW) del MIT
desde 2008. El sitio incluye grabaciones de video de conferencias y un conjunto
completo de conjuntos de problemas y exámenes. Desde el otoño de 2012, edX y
MITx ofrecen una versión en línea de este curso. Recomendamos encarecidamente
que realice los conjuntos de problemas asociados con una de las ofertas de OCW o
edX.

1Esta fue la supuesta respuesta de Euclides, alrededor del año 300 a. C., a la solicitud del
rey Ptolomeo de una forma más fácil de aprender matemáticas.
EXPRESIONES DE GRATITUD

Este libro surgió de un conjunto de notas de conferencias que preparé mientras


impartía un curso de pregrado en el MIT. El curso, y por lo tanto este libro, se
benefició de las sugerencias de los colegas de la facultad (especialmente Eric
Grimson, Srinivas Devadas y Fredo Durand), los asistentes de enseñ anza y los
estudiantes que tomaron el curso.

El proceso de transformar mis notas de clase en un libro resultó ser mucho má s


oneroso de lo que esperaba. Afortunadamente, este optimismo equivocado duró lo
suficiente como para evitar que me rindiera. El aliento de mis colegas y mi familia
tambié n me ayudó a seguir adelante.

Eric Grimson, Chris Terman y David Guttag brindaron una ayuda vital. Eric, que es el
Canciller del MIT, logró encontrar el tiempo para leer casi todo el libro con mucho
cuidado. Encontró numerosos errores (incluido un nú mero vergonzoso, para mí, de
errores técnicos) y señ aló los lugares donde faltaban las explicaciones necesarias.
Chris también leyó partes del manuscrito y descubrió errores. También me ayudó a
luchar contra Microsoft Word, al que finalmente convencimos para que hiciera la
mayor parte de lo que queríamos. David superó su aversió n a la informá tica y
corrigió varios capítulos.

Las versiones preliminares de este libro se utilizaron en el curso MIT 6.00 y el curso
MITx 6.00x. Varios estudiantes de estos cursos señ alaron errores. Un estudiante de
6.00x, JC Cabrejas, fue particularmente ú til. Encontró una gran cantidad de errores
tipográ ficos y má s de unos pocos errores té cnicos.

Como todos los profesores exitosos, les debo mucho a mis estudiantes de
posgrado. La foto en la contraportada de este libro me muestra apoyando a
algunos de mis estudiantes actuales. En el laboratorio, sin embargo, son ellos
quienes me apoyan. Ademá s de hacer una excelente investigació n (y permitirme
tomar parte del cré dito por ello), Guha Balakrishnan, Joel Brooks, Ganeshapillai
Gartheeban, Jen Gong, Yun Liu, Anima Singh, Jenna Wiens y Amy Zhao brindaron
comentarios ú tiles sobre este manuscrito. .

Tengo una deuda especial de gratitud con Julie Sussman, PPA Hasta que comencé a
trabajar con Julie, no tenía idea de la diferencia que podía hacer un editor. Había
trabajado con correctores de estilo en libros anteriores y pensé que eso era lo que
necesitaba para este libro. Me equivoqué. Necesitaba un colaborador que pudiera
leer el libro con los ojos de un estudiante y decirme lo que había que hacer, lo que
debería hacerse y lo que podría hacerse si tuviera el tiempo y la energía para hacerlo.
Julie me enterró en "sugerencias" que eran demasiado buenas para ignorarlas. Su
dominio combinado del idioma inglés y la programació n es bastante notable.

Finalmente, gracias a mi esposa, Olga, por empujarme a terminar y por ayudarme en


los momentos críticos.
1 EMPEZANDO

Una computadora hace dos cosas, y só lo dos cosas: realiza cá lculos y recuerda los
resultados de esos cá lculos. Pero hace esas dos cosas extremadamente bien. La
computadora típica que se sienta en un escritorio o en un maletín realiza
aproximadamente mil millones de cá lculos por segundo. Es difícil imaginar lo
realmente rá pido que es. Piensa en sostener una pelota a un metro del suelo y
soltarla. Para cuando llegue al piso, su computadora podría haber ejecutado má s
de mil millones de instrucciones. En cuanto a la memoria, una computadora
típica puede tener cientos de gigabytes de almacenamiento. ¿Como de grande es?
Si un byte (la cantidad de bits, normalmente ocho, necesarios para representar
un cará cter) pesara una onza (que no es así), 100 gigabytes pesarían má s de
3.000.000 de toneladas. A modo de comparació n, ese es aproximadamente el
peso de todo el carbó n producido en un añ o en los EE. UU.

Durante la mayor parte de la historia humana, la computació n estuvo limitada


por la velocidad de cá lculo del cerebro humano y la capacidad de registrar
resultados computacionales con la mano humana. Esto significaba que solo los
problemas má s pequeñ os podían ser atacados computacionalmente. Incluso con
la velocidad de las computadoras modernas, todavía hay problemas que está n
má s allá de los modelos computacionales modernos (por ejemplo, comprender el
cambio climá tico), pero cada vez má s problemas está n demostrando ser
susceptibles de solució n computacional. Esperamos que cuando termine este
libro, se sienta có modo utilizando el pensamiento computacional para resolver
muchos de los problemas que encuentre durante sus estudios, trabajo e incluso
la vida cotidiana.

¿Qué entendemos por pensamiento computacional?

Todo conocimiento puede considerarse declarativo o imperativo. El conocimiento


declarativo se compone de enunciados de hecho. Por ejemplo, "la raíz cuadrada de x
es un nú mero y tal que y*y = x". Esta es una declaració n de hecho.
Desafortunadamente, no nos dice có mo encontrar una raíz cuadrada.

conocimiento imperativoes conocimiento de "có mo hacer", o recetas para


deducir informació n. Heron of Alexandria fue el primero en documentar una forma
de calcular la raíz cuadrada de un nú mero.2 Su método se puede resumir de la
siguiente manera:

 Comience con una conjetura,gramo.


 Sig*gestá lo suficientemente cerca deX, para y di esogramoes la respuesta.
 De lo contrario, cree una nueva conjetura promediando gramoyx/g, es decir,(g + x/g)/2.
 Usando esta nueva conjetura, que nuevamente llamamos gramo, repite el proceso
hastag*g
está lo suficientemente cerca de x.

2Muchos creen que Heron no fue el inventor de este mé todo y, de hecho, hay alguna
evidencia de que los antiguos babilonios lo conocían bien.
2 Capítulo 1. Primeros pasos

Considere, por ejemplo, encontrar la raíz cuadrada de 25.

1. Colocargramoa algú n valor arbitrario, por ejemplo,3.


2. Nosotros decidimos que3*3 = 9no está lo suficientemente cerca de25
3. Establecerg a (3 + 25/3)/2 = 5,67.3
4. Decidimos que5,67*5,67 = 32,15todavía no está lo suficientemente cerca de25
5. Establecerg a (5,67 + 25/5,67)/2 = 5,04
6. Decidimos que5,04*5,04 = 25,4está lo suficientemente cerca, así que nos detenemos y
declaramos5.04
ser una aproximació n adecuada a la raíz cuadrada de 25.
Tenga en cuenta que la descripció n del mé todo es una secuencia de pasos
simples, junto con un flujo de control que especifica cuá ndo se debe ejecutar
cada paso. Tal descripció n se llama algoritmo.4 Este algoritmo es un ejemplo de
un algoritmo de adivinar y verificar. Se basa en el hecho de que es fá cil
comprobar si una conjetura es buena o no.

Un poco má s formalmente, un algoritmo es una lista finita de instrucciones que


describen un cá lculo que, cuando se ejecuta en un conjunto de entradas
proporcionado, pasará por un conjunto de estados bien definidos y finalmente
producirá una salida.

Un algoritmo es un poco como una receta de un libro de cocina:

1. Ponga la mezcla de natillas sobre el fuego.


2. Remover.
3. Moje la cuchara en la crema pastelera.
4. Retire la cuchara y pase el dedo por el dorso de la cuchara.
5. Si queda un camino despejado, retire las natillas del fuego y dé jelas enfriar.
6. De lo contrario repetir.
Incluye algunas pruebas para decidir cuá ndo se completa el proceso, así como
instrucciones sobre el orden en que ejecutar las instrucciones, a veces saltando a
alguna instrucció n basada en una prueba.

Entonces, ¿có mo se captura esta idea de una receta en un proceso mecá nico? Una
forma sería diseñ ar una má quina diseñ ada específicamente para calcular raíces
cuadradas.
Por extrañ o que parezca, las primeras má quinas informá ticas eran, de hecho,
computadoras de programa fijo, lo que significa que estaban diseñ adas para hacer
cosas muy específicas y, en su mayoría, eran herramientas para resolver un
problema matemá tico específico, por ejemplo, para calcular la trayectoria de una
artillería. caparazó n. Una de las primeras computadoras (construida en 1941 por
Atanasoff y Berry) resolvía sistemas de ecuaciones lineales, pero no podía hacer nada
má s. La má quina bombe de Alan Turing, desarrollada durante la Segunda Guerra
Mundial, fue diseñ ada estrictamente con el propó sito de descifrar los có digos Enigma
alemanes. Algunas computadoras muy simples todavía usan este enfoque. Por

ejemplo, una calculadora de cuatro funciones es una computadora de programa fijo.


Puede hacer aritmé tica bá sica, pero no puede

3Para simplificar, estamos redondeando los resultados.


4La palabra “algoritmo” se deriva del nombre del matemá tico persa Muhammad ibn Musa
al-Khwarizmi.
Capítulo 1. Primeros 3

utilizarse como procesador de textos o para ejecutar videojuegos. Para cambiar


el programa de tal má quina, uno tiene que reemplazar el circuito.

La primera computadora verdaderamente moderna fue la Manchester Mark 1.5.


Se distinguía de sus predecesoras por el hecho de que era una computadora con
programa almacenado. Tal computadora almacena (y manipula) una secuencia
de instrucciones y tiene un conjunto de elementos que ejecutará n cualquier
instrucció n en esa secuencia. Al crear una arquitectura de conjunto de
instrucciones y detallar el cá lculo como una secuencia de instrucciones (es
decir, un programa), creamos una má quina altamente flexible. Al tratar esas
instrucciones de la misma manera que los datos, una má quina de programa
almacenado puede cambiar fá cilmente el programa y puede hacerlo bajo el
control del programa. De hecho, el corazó n de la computadora se convierte en
un programa (llamado inté rprete) que puede ejecutar cualquier conjunto legal
de instrucciones y, por lo tanto, puede usarse para calcular cualquier cosa que
uno pueda describir usando un conjunto bá sico de instrucciones.

Tanto el programa como los datos que manipula residen en la memoria. Por lo
general, hay un contador de programa que apunta a una ubicació n particular en
la memoria y el cá lculo comienza con la ejecució n de la instrucció n en ese punto.
La mayoría de las veces, el inté rprete simplemente pasa a la siguiente instrucció n
de la secuencia, pero no siempre. En algunos casos, realiza una prueba y, sobre la
base de esa prueba, la ejecució n puede saltar a algú n otro punto en la secuencia
de instrucciones. Esto se llama flujo de control y es esencial para permitirnos
escribir programas que realicen tareas complejas.

Volviendo a la metá fora de la receta, dado un conjunto fijo de ingredientes, un buen


chef puede preparar un nú mero ilimitado de sabrosos platos combiná ndolos de
diferentes maneras. De manera similar, dado un pequeñ o conjunto fijo de elementos
primitivos, un buen programador puede producir un nú mero ilimitado de programas
ú tiles. Esto es lo que hace que la programació n sea un esfuerzo tan asombroso.

Para crear recetas, o secuencias de instrucciones, necesitamos un lenguaje de


programació n en el que describir estas cosas, una forma de darle a la
computadora sus ó rdenes de marcha.

En 1936, el matemá tico britá nico Alan Turing describió un dispositivo informá tico
hipoté tico que se ha dado en llamar Má quina Universal de Turing. La má quina
tenía una memoria ilimitada en forma de cinta en la que se podían escribir ceros y
unos, y algunas instrucciones primitivas muy sencillas para mover, leer y escribir
en la cinta. La tesis de Church-Turing establece que si una funció n es computable,
se puede programar una má quina de Turing para calcularla.

El “si” en la tesis de Church-Turing es importante. No todos los problemas tienen


soluciones computacionales. Por ejemplo, Turing demostró que es imposible escribir
un programa que, dado un programa arbitrario, llá melo P, imprima verdadero si y
solo si P se ejecutará para siempre. Esto se conoce como el problema de la detenció n.

5Esta computadora fue construida en la Universidad de Manchester y ejecutó su primer


programa en 1949. Implementó ideas descritas previamente por John von Neumann y fue
anticipada por el concepto teó rico de la Má quina Universal de Turing descrito por Alan Turing
en 1936.
4 Capítulo 1. Primeros pasos

La tesis de Church-Turing conduce directamente a la noció n de completitud de


Turing. Se dice que un lenguaje de programació n es Turing completo si se puede
usar para simular una má quina de Turing universal. Todos los lenguajes de
programació n modernos son Turing completos. Como consecuencia, cualquier
cosa que pueda programarse en un lenguaje de programació n (p. ej., Python)
puede programarse en cualquier otro lenguaje de programació n (p. ej., Java).
Por supuesto, algunas cosas pueden ser má s fá ciles de programar en un lenguaje
en particular, pero todos los lenguajes son fundamentalmente iguales con
respecto al poder computacional.

Afortunadamente, ningú n programador tiene que construir programas a partir de


las instrucciones primitivas de Turing. En cambio, los lenguajes de programació n
modernos ofrecen un conjunto de primitivos má s grande y conveniente. Sin
embargo, la idea fundamental de la programació n como el proceso de ensamblar
una secuencia de operaciones sigue siendo central.

Cualquiera que sea el conjunto de primitivas que uno tenga, y cualquiera que sean
los métodos que uno tenga para usarlas, lo mejor y lo peor de la programació n son
lo mismo: la computadora hará exactamente lo que le digas que haga. Esto es
bueno porque significa que puedes hacer que haga todo tipo de cosas divertidas y
ú tiles. Es algo malo porque cuando no hace lo que quieres que haga, normalmente
no tienes a nadie a quien culpar sino a ti mismo.

Hay cientos de lenguajes de programació n en el mundo. No existe el mejor lenguaje


(aunque uno podría nominar algunos candidatos para el peor). Diferentes idiomas
son mejores o peores para diferentes tipos de aplicaciones. MATLAB, por ejemplo,
es un excelente lenguaje para manipular vectores y matrices. C es un buen lenguaje
para escribir los programas que controlan las redes de datos. PHP es un buen
lenguaje para construir sitios Web. Y Python es un buen lenguaje de propó sito
general.

Cada lenguaje de programació n tiene un conjunto de construcciones primitivas, una


sintaxis, una semá ntica está tica y una semá ntica. Por analogía con un lenguaje
natural, por ejemplo, el inglé s, las construcciones primitivas son palabras, la sintaxis
describe qué cadenas de palabras constituyen oraciones bien formadas, la
semá ntica está tica define qué oraciones tienen significado y la semá ntica define el
significado de esas oraciones. Las construcciones primitivas en Python incluyen
literales (p. ej., el nú mero 3.2 y la cadena 'abc') y operadores infijos (p. ej., + y /).

La sintaxis de un lenguaje define qué cadenas de caracteres y símbolos está n


bien formadas. Por ejemplo, en inglé s la cadena “Cat dog boy”. no es una
oració n sintá cticamente vá lida, porque la sintaxis del inglé s no acepta
oraciones de la forma <sustantivo> <sustantivo> <sustantivo>. En Python, la
secuencia de primitivas 3.2 + 3.2 está bien formada sintá cticamente, pero la
secuencia 3.2 3.2 no lo está .

La semá ntica está tica define qué cadenas sintá cticamente vá lidas tienen un
significado. En inglé s, por ejemplo, la cadena “I are big” tiene la forma
<pronombre> <verbo de enlace> <adjetivo>, que es una secuencia
sintá cticamente aceptable. Sin embargo, no es vá lido en inglé s, porque el
sustantivo “I” es singular y el verbo “are” es plural. Este es un ejemplo de un
error semá ntico está tico. En Python, la secuencia 3.2/'abc' está sintá cticamente
bien formada (<literal> <operador> <literal>), pero
Capítulo 1. Primeros 5

produce un error semá ntico está tico ya que no tiene sentido dividir un nú mero por
una cadena de caracteres.

La semá ntica de un lenguaje asocia un significado con cada cadena de símbolos


sintá cticamente correcta que no tiene errores semá nticos está ticos. En lenguajes
naturales, la semá ntica de una oració n puede ser ambigua. Por ejemplo, la
oració n "No puedo elogiar demasiado a este estudiante", puede ser halagador o
condenatorio. Los lenguajes de programació n está n diseñ ados para que cada
programa legal tenga exactamente un significado.

Aunque los errores de sintaxis son el tipo de error má s comú n (especialmente


para aquellos que está n aprendiendo un nuevo lenguaje de programació n), son el
tipo de error menos peligroso. Todos los lenguajes de programació n serios
realizan un trabajo completo de detecció n de errores sintá cticos y no permitirá n
que los usuarios ejecuten un programa con un solo error sintá ctico. Ademá s, en la
mayoría de los casos, el sistema de idioma brinda una indicació n suficientemente
clara de la ubicació n del error que es obvio lo que se debe hacer para
solucionarlo.

La situació n con respecto a los errores semá nticos está ticos es un poco má s
compleja. Algunos lenguajes de programació n, por ejemplo, Java, realizan
muchas comprobaciones semá nticas está ticas antes de permitir que se ejecute
un programa. Otros, por ejemplo, C y Python (por desgracia), hacen una
verificació n semá ntica relativamente menos está tica. Python hace una cantidad
considerable de verificació n semá ntica está tica mientras ejecuta un programa.
Sin embargo, no detecta todos los errores semá nticos está ticos. Cuando estos
errores no se detectan, el comportamiento de un programa suele ser
impredecible. Veremos ejemplos de esto má s adelante en el libro.

Normalmente no se habla de un programa como si tuviera un error semá ntico. Si


un programa no tiene errores sintá cticos ni errores semá nticos está ticos, tiene
un significado, es decir, tiene semá ntica. Por supuesto, eso no quiere decir que
tenga la semá ntica que su creador pretendía que tuviera. Cuando un programa
significa algo diferente de lo que su creador cree que significa, pueden suceder
cosas malas.

¿Qué podría pasar si el programa tiene un error y se comporta de forma no


deseada?

 Podría bloquearse, es decir, dejar de ejecutarse y producir algú n tipo de


indicació n obvia de que lo ha hecho. En un sistema informá tico
correctamente diseñ ado, cuando un programa falla, no dañ a el sistema en
general. Por supuesto, algunos sistemas informá ticos muy populares no
tienen esta agradable propiedad. Casi todos los que usan una
computadora personal han ejecutado un programa que ha logrado que
sea necesario reiniciar toda la computadora.

 O podría seguir funcionando, y funcionando, y funcionando, y nunca


detenerse. Si uno no tiene idea de cuá nto tiempo se supone que el programa
tardará aproximadamente en hacer su trabajo, esta situació n puede ser
difícil de reconocer.

 O podría ejecutarse hasta completarse y producir una respuesta que


podría ser correcta o no.
6 Capítulo 1. Primeros pasos

Cada uno de estos es malo, pero el ú ltimo de ellos es ciertamente el peor.


Cuando un programa parece estar haciendo lo correcto pero no lo hace, pueden
seguir cosas malas. Se pueden perder fortunas, los pacientes pueden recibir
dosis fatales de radioterapia, los aviones pueden estrellarse, etc.

Siempre que sea posible, los programas deben escribirse de tal manera que
cuando no funcionen correctamente, sea evidente. Discutiremos có mo hacer esto
a lo largo del libro.

Ejercicio de dedo: Las computadoras pueden ser fastidiosamente literales. Si no


les dice exactamente lo que quiere que hagan, es probable que hagan algo
incorrecto. Intente escribir un algoritmo para conducir entre dos destinos.
Escríbalo como lo haría para una persona, y luego imagine lo que sucedería si esa
persona ejecutara el algoritmo exactamente como está escrito. Por ejemplo,
¿cuá ntas multas de trá fico podrían recibir?
2. INTRODUCCIÓN A PITÓN

Aunque cada lenguaje de programació n es diferente (aunque no tan diferente como


sus diseñ adores nos quieren hacer creer), hay algunas dimensiones a lo largo de las
cuales se pueden relacionar.

 Bajo nivel versus alto nivelse refiere a si programamos usando


instrucciones y objetos de datos al nivel de la má quina (por ejemplo,
mover 64 bits de datos de esta ubicació n a esa ubicació n) o si
programamos usando operaciones má s abstractas (por ejemplo, abrir
un menú en la pantalla) proporcionados por el diseñ ador del lenguaje.
 General versus dirigido a un dominio de aplicaciónse refiere a si las
operaciones primitivas del lenguaje de programació n son ampliamente
aplicables o está n ajustadas a un dominio. Por ejemplo, Adobe Flash está
diseñ ado para facilitar la adició n de animació n e interactividad a las pá ginas
web, pero no querrá usarlo para crear un programa de aná lisis de cartera de
acciones.
 Interpretado versus compiladose refiere a si la secuencia de instrucciones
escritas por el programador, llamada có digo fuente, se ejecuta directamente
(por un inté rprete) o si primero se convierte (por un compilador) en una
secuencia de operaciones primitivas a nivel de má quina. (En los primeros
días de las computadoras, la gente tenía que escribir el có digo fuente en un
lenguaje que estaba muy cerca del có digo de má quina que podía ser
interpretado directamente por el hardware de la computadora). Ambos
enfoques tienen ventajas. A menudo es má s fá cil depurar programas
escritos en lenguajes que está n diseñ ados para ser interpretados, porque el
inté rprete puede generar mensajes de error que son fá ciles de correlacionar
con el có digo fuente. Los lenguajes compilados generalmente producen
programas que se ejecutan má s rá pido y usan menos espacio.
En este libro, usamos Python. Sin embargo, este libro no es sobre Python. Sin duda,
ayudará a los lectores a aprender Python, y eso es algo bueno. Sin embargo, lo que es
mucho má s importante es que los lectores cuidadosos aprenderá n algo sobre có mo
escribir programas que resuelvan problemas. Esta habilidad se puede transferir a
cualquier lenguaje de programació n.

Python es un lenguaje de programació n de propó sito general que se puede usar de


manera efectiva para construir casi cualquier tipo de programa que no necesite
acceso directo al hardware de la computadora. Python no es ó ptimo para
programas que tienen restricciones de alta confiabilidad (debido a su débil
verificació n semá ntica está tica) o que son construidos y mantenidos por muchas
personas o durante un largo período de tiempo (nuevamente debido a la dé bil
verificació n semá ntica está tica).

Sin embargo, Python tiene varias ventajas sobre muchos otros lenguajes. Es un
lenguaje relativamente simple que es fá cil de aprender. Debido a que Python está
diseñ ado para ser interpretado, puede proporcionar el tipo de retroalimentació n en
tiempo de ejecució n que es especialmente ú til para los programadores novatos.
También hay una gran cantidad de bibliotecas disponibles gratuitamente que
interactú an con Python y brindan una funcionalidad extendida ú til.
Varios de ellos se utilizan en este libro.
8 Capítulo 2. Introducción a

Ahora estamos listos para comenzar a aprender algunos de los elementos bá sicos de
Python. Estos son comunes a casi todos los lenguajes de programació n en concepto,
aunque no necesariamente en detalle.

Se debe advertir al lector que este libro no es de ninguna manera una introducció n
completa a Python. Usamos Python como vehículo para presentar conceptos
relacionados con la resolució n de problemas computacionales y el pensamiento. El
lenguaje se presenta a cuentagotas, segú n sea necesario para este propó sito ulterior.
Las características de Python que no necesitamos para ese propó sito no se presentan
en absoluto. Nos sentimos có modos al no cubrir todo el idioma porque hay
excelentes recursos en línea que describen casi todos los aspectos del idioma.
Cuando enseñ amos el curso en el que se basa este libro, sugerimos a los estudiantes
que confíen en estos recursos gratuitos en línea para obtener material de referencia
de Python.

Python es un lenguaje vivo. Desde su introducció n por Guido von Rossum en


1990, ha sufrido muchos cambios. Durante la primera dé cada de su vida, Python
fue un lenguaje poco conocido y poco utilizado. Eso cambió con la llegada de
Python 2.0 en 2000. Ademá s de incorporar una serie de mejoras importantes al
lenguaje en sí, marcó un cambio en el camino evolutivo del lenguaje. Una gran
cantidad de personas comenzaron a desarrollar bibliotecas que interactuaban sin
problemas con Python, y el apoyo y desarrollo continuos del ecosistema de
Python se convirtió en una actividad comunitaria. Python 3.0 se lanzó a fines de
2008. Esta versió n de Python eliminó muchas de las inconsistencias en el diseñ o
de las diversas versiones de Python 2 (a menudo denominada Python 2.x). Sin
embargo, no era compatible con versiones anteriores.

La incompatibilidad hacia atrá s presenta un problema para este libro. Desde


nuestro punto de vista, Python 3.0 es claramente superior a Python 2.x. Sin
embargo, en el momento de escribir este artículo, algunas bibliotecas importantes
de Python aú n no funcionan con Python 3. Por lo tanto, utilizaremos Python 2.7 (en
el que muchas de las características má s importantes de Python 3 han sido
"reportadas") a lo largo de este libro.

2.1 Los elementos básicos de Python


Un programa de Python, a veces llamado script, es una secuencia de definiciones
y comandos. Estas definiciones son evaluadas y los comandos son ejecutados
por el inté rprete de Python en algo llamado shell. Por lo general, se crea un
nuevo shell cada vez que comienza la ejecució n de un programa. En la mayoría
de los casos, una ventana está asociada con el shell.

Recomendamos que inicie un shell de Python ahora y lo use para probar los
ejemplos contenidos en el resto del capítulo. Y, para el caso, también má s adelante
en el libro.

Un comando, a menudo llamado declaració n, instruye al inté rprete para que haga
algo. Por ejemplo, la declaració nimprime '¡Regla de los yanquis!'le indica al
inté rprete que emita la cadena¡Regla de los yanquis!a la ventana asociada con el
shell.
Capitulo 2. Introducción a 9

La secuencia de comandos
imprime '¡Regla de los
yanquis!' print '¡Pero no
en Boston!'
print 'Yankees gobiernan', '¡pero no en Boston!'

hace que el intérprete produzca la salida


¡Regla de los yanquis!
¡Pero no en Boston!
¡Los yanquis gobiernan, pero no en Boston!

Observe que se pasaron dos valores para imprimir en la tercera declaració n. El


comando de impresió n toma un nú mero variable de valores y los imprime, separados
por un cará cter de espacio, en el orden en que aparecen.6

2.1.1 Objetos, expresiones y tipos numéricos


Objetosson las cosas centrales que manipulan los programas de Python. Cada
objeto tiene un tipo que define el tipo de cosas que los programas pueden hacer
con objetos de ese tipo.

Los tipos son escalares o no escalares. Los objetos escalares son indivisibles.
Piense en ellos como los á tomos del lenguaje.7 Los objetos no escalares, por
ejemplo, las cadenas, tienen una estructura interna.

Python tiene cuatro tipos de objetos escalares:

 En tse utiliza para representar nú meros enteros. literales de tipoEn tse


escriben en la forma en que normalmente denotamos nú meros enteros (p. ej.,
-3o5o10002).
 flotarse utiliza para representar nú meros reales. literales de
tipoflotarincluya siempre un punto decimal (p. ej.,3.0o3.17o-28.72).
(Tambié n es posible escribir literales de tipoflotarusando notació n científica.
Por ejemplo, el literal1.6E3representa1,6*103, es decir, es lo mismo
que1600.0.) Quizá s se pregunte por qué este tipo no se llamareal. Dentro de
la computadora, los valores de tipo flotarse almacenan en la computadora
como nú meros de coma flotante. Esta representació n, que utilizan todos los
lenguajes de programació n modernos, tiene muchas ventajas. Sin embargo,
en algunas situaciones hace que la aritmética de punto flotante se comporte
de formas ligeramente diferentes a la aritmética de nú meros reales.
Discutimos esto en la Secció n 3.4.
 boolse utiliza para representar los valores booleanos VerdaderoyFALSO.

 Ningunoes un tipo con un solo valor. Diremos má s sobre esto cuando


lleguemos a las variables.
Los objetos y operadores se pueden combinar para formar expresiones, cada
una de las cuales se evalú a como un objeto de algú n tipo. Nos referiremos a esto
como el valor de la expresió n. Por ejemplo, la expresió n 3 + 2 denota el objeto 5
de tipo int y la expresió n 3.0 + 2.0 denota el objeto 5.0 de tipo float.

6EnPython 3, imprimir es una funció n en lugar de un comando. Por lo tanto, uno escribiría
print('¡Gobiernan los Yankees!', 'pero no en Boston').
7Sí,
los á tomos no son verdaderamente indivisibles. Sin embargo, dividirlos no es fá cil y
hacerlo puede tener consecuencias que no siempre son deseables.
1 Capítulo 2. Introducción a

El operador == se usa para probar si dos expresiones dan como resultado el mismo
valor, y el operador != se usa para probar si dos expresiones dan como resultado
valores diferentes.

El símbolo >>> es un indicador de shell que indica que el intérprete espera que el
usuario escriba algú n có digo de Python en el shell. La línea debajo de la línea con el
indicador se produce cuando el intérprete evalú a el có digo de Python ingresado en
el indicador, como se ilustra en la siguiente interacció n con el inté rprete:
>>> 3 + 2
5
>>> 3.0 + 2.0
5.0
>>> 3 != 2
Verdadero

El tipo de funció n integrada de Python se puede utilizar para averiguar el tipo de un objeto:
>>> tipo(3)
<escriba 'int'>
>>> tipo(3.0)
<escriba 'flotador'>

Los operadores de tipos int y float se enumeran en la figura 2.1.

 Figura
yo+jes la suma dei2.1
yj.Operadores
Siiyjambosen tipos
son tyflotar
En En
de tipo t, el resultado es
unEn t. Si alguno de ellos es unflotar, el resultado es unflotar.
Los operadores aritmé ticos tienen la precedencia habitual. Por ejemplo, * se une
 yo-jesimenos
má s estrechamente quej+,. Si
pori yj
loambos
que lason de tipo
expresió n En t, el
x+y*2 seresultado es unEn t.
evalú a multiplicando
Si alguno de ellos es unflotar, el resultado es unflotar.
primero y por 2 y luego sumando el resultado a x. El orden de evaluació n puede ser
cambiado
 i*j por
es el producto de i y j. Si i y j son del tipo int, el resultado es un int.
Si cualquiera de ellos es un flotante, el resultado es un flotante.
 yo//jes divisió n entera. Por ejemplo, el valor de 6//2 es el int 3 y el
valor de 6//4 es el int 1. El valor es 1 porque la divisió n de enteros
devuelve el cociente e ignora el resto.
 yo/jes i dividido por j. En Python 2.7, cuando i y j son del tipo int, el
resultado también es un int; de lo contrario, el resultado es un float. En
este libro, nunca usaremos / para dividir un int por otro. Usaremos //
para hacer eso. (En Python 3, el operador /, gracias a Dios, siempre
devuelve un flotante. Por ejemplo, en Python 3 el valor de 6/4 es 1,5).
 yo% jes el resto cuando el int i se divide por el int j. Por lo general,
se pronuncia "i mod j", que es la abreviatura de "i modulo j".
 i**jesielevado a the poderj. Siiyjambos son de tipoEn t, el
resultado es unEn t. Si alguno de ellos es unflotar, el resultado es
unflotar.
 Los operadores de comparació n son == (igual), != (no igual), > (mayor),
>=(al menos), <, (menos) y <= (como má ximo).
Capitulo 2. Introducción a 1

usar paré ntesis para agrupar subexpresiones, por ejemplo, (x+y)*2 primero suma x e
y, y luego multiplica el resultado por 2.

Los operadores en tipo bool son:

 a y BesVerdaderosi ambosaybsonVerdadero, yFALSOde lo contrario.

 a o BesVerdaderosi al menos uno deaobesVerdadero, yFALSOde lo contrario.

 No unesVerdaderosiaesFALSO, yFALSOsiaesVerdadero.

2.1.2 Variables y asignación


Variablesproporcionar una forma de asociar nombres con objetos. Considere el có digo
pi = 3
radio = 11
área = pi * (radio**2)
radio = 14

Primero vincula los nombres pi8 y radio a diferentes objetos de tipo int. Luego vincula
el á rea del nombre a un tercer objeto de tipo int. Esto se representa en el panel
izquierdo de la Figura 2.2.

Figura 2.2 Vinculación de variables a objetos

Si el programa luego ejecuta radio = 11, el nombre radio se vuelve a vincular a un


objeto diferente de tipo int, como se muestra en el panel derecho de la Figura 2.2.
Tenga en cuenta que esta asignació n no tiene ningú n efecto sobre el valor al que está
vinculada el á rea. Todavía está vinculado al objeto indicado por la expresió n
3*(11**2).

En Python, una variable es solo un nombre, nada má s. Recuerde esto: es importante.


Una declaració n de asignació n asocia el nombre a la izquierda del símbolo = con el
objeto denotado por la expresió n a la derecha del =.
Recuerda esto tambié n. Un objeto puede tener uno, má s de uno o ningú n
nombre asociado.

8Sicree que el valor real deπno es 3, tienes razó n. Incluso demostramos ese hecho en el
Capítulo 15.
1 Capítulo 2. Introducción a

Quizá s no deberíamos haber dicho, “una variable es solo un nombre”. A pesar de


lo que dijo Julieta, 9 nombres importan. Los lenguajes de programació n nos
permiten describir los cá lculos de una manera que permite que las má quinas los
ejecuten. Esto no significa que solo las computadoras lean programas.

Como pronto descubrirá , no siempre es fá cil escribir programas que funcionen


correctamente. Los programadores experimentados confirmará n que pasan mucho
tiempo leyendo programas en un intento de entender por qué se comportan como lo
hacen. Por lo tanto, es de vital importancia escribir programas de tal manera que
sean fá ciles de leer. La elecció n adecuada de nombres de variables juega un papel
importante en la mejora de la legibilidad.

Considere los dos fragmentos de có digo

un =3.14159pi = 3.14159
segundo =11,2 de diámetro = 11,2
c =un*(b**2)área = pi*(diámetro**2)

En lo que respecta a Python, no son diferentes. Cuando se ejecuten, hará n lo


mismo. Para un lector humano, sin embargo, son bastante diferentes. Cuando
leemos el fragmento de la izquierda, no hay razó n a priori para sospechar que
algo anda mal. Sin embargo, un vistazo rá pido al có digo de la derecha debería
hacernos sospechar que algo anda mal. O la variable debería haberse llamado
radio en lugar de diá metro, o el diá metro debería haberse dividido por 2,0 en el
cá lculo del á rea.

En Python, los nombres de variables pueden contener letras mayú sculas y


minú sculas, dígitos (pero no pueden comenzar con un dígito) y el cará cter
especial_.Los nombres de las variables de Python distinguen entre mayú sculas y
minú sculas, por ejemplo, Julie y julie son nombres diferentes. Finalmente, hay
una pequeñ a cantidad de palabras reservadas (a veces llamadas palabras clave)
en Python que tienen significados incorporados y no se pueden usar como
nombres de variables. Las diferentes versiones de Python tienen listas
ligeramente diferentes de palabras reservadas. Las palabras reservadas en
Python 2.7
sony,como,afirmar,romper,clase,continuar,definitivamente,del,elif,demás,e
xcepto,ejecutivo,finalmente,para,de,global,si,importar,en,es,lambda,no,o,ap
robar,imprimir,aumentar,devolver,intentar,con,mientras, yproducir.

Otra buena manera de mejorar la legibilidad del có digo es agregar comentarios.


Python no interpreta el texto que sigue al símbolo #. Por ejemplo, uno podría
escribir
#restar el área del cuadrado s del área del
círculo c áreaC = pi*radio**2
áreaS = lado*diferencia
de lado = áreaC-áreaS

Python permite la asignació n mú ltiple. La declaració n


x, y = 2, 3

une x a 2 e y a 3. Todas las expresiones en el lado derecho de la asignació n se


evalú an antes de que se cambien los enlaces. esto es conveniente

9"¿Lo que hay en un nombre? Lo que llamamos rosa con cualquier otro nombre olería igual de dulce”.
Capitulo 2. Introducción a 1

ya que le permite usar asignació n mú ltiple para intercambiar los enlaces de dos
variables.

Por ejemplo, el có digo


x, y = 2, 3
x, y = y, x
imprime 'x =',
x imprime 'y
=', y

imprimirá
x = 3
y = 2

2.1.3 INACTIVO
Escribir programas directamente en el shell es muy inconveniente. La mayoría de
los programadores prefieren usar algú n tipo de editor de texto que sea parte de un
entorno de desarrollo integrado (IDE).

En este libro, usaremos IDLE,10 el IDE que viene como parte del paquete de
instalació n está ndar de Python. IDLE es una aplicació n, como cualquier otra
aplicació n en su computadora. Inícielo de la misma manera que iniciaría
cualquier otra aplicació n, por ejemplo, haciendo doble clic en un icono.

IDLE proporciona

 un editor de texto con resaltado de sintaxis, autocompletado y


sangría inteligente,
 un shell con resaltado de sintaxis, y
 un depurador integrado, que debe ignorar por ahora.
Cuando se inicia IDLE, se abrirá una ventana de shell en la que puede escribir
comandos de Python. Tambié n le proporcionará un menú de archivo y un menú de
edició n (así como algunos otros menú s, que puede ignorar con seguridad por
ahora).

El menú de archivo incluye comandos para

 crear una nueva ventana de edició n en la que puede escribir un programa de


Python,

 abra un archivo que contenga un programa de Python existente, y

 guardar el contenido de la ventana de edició n actual en un archivo


(con extensió n de archivo.py).
El menú de edició n incluye comandos de edició n de texto está ndar (p. ej., copiar,
pegar y buscar) ademá s de algunos comandos diseñ ados específicamente para
facilitar la edició n del có digo de Python (p. ej., aplicar sangría a la regió n y comentar
la regió n).

10Supuestamente, el nombre Python fue elegido como tributo a la compañ ía de


comedia britá nica Monty Python. Esto lleva a pensar que el nombre IDLE es un
juego de palabras con Eric Idle, miembro de la compañ ía.
1 Capítulo 2. Introducción a

Para obtener una descripció n completa de IDLE, consulte


http://docs.python.org/library/idle.html .

2.2 Programas de ramificación


Los tipos de cá lculos que hemos estado viendo hasta ahora se llaman programas
de línea recta. Ejecutan una instrucció n tras otra en el orden en que aparecen y
se detienen cuando se quedan sin instrucciones. Los tipos de cá lculos que
podemos describir con programas de línea recta no son muy interesantes. De
hecho, son francamente aburridos.

DerivaciónLos programas son má s interesantes. La declaració n de ramificació n


má s simple es una condicional. Como se muestra en la Figura 2.3, una
declaració n condicional tiene tres partes:

 una prueba, es decir, una expresió n que se evalú a comoVerdaderooFALSO;


 un bloque de có digo que se ejecuta si la prueba se evalú a comoVerdadero; y
 un bloque opcional de có digo que se ejecuta si la prueba se evalú a comoFALSO.
Despué s de que se ejecuta una declaració n condicional, la ejecució n se reanuda en el
có digo que sigue a la declaració n.

Figura 2.3 Diagrama de flujo para declaración condicional

En Python, una declaració n condicional tiene la forma


siexpresión
booleana:bloque de
código
demás:
bloque de código

Al describir la forma de las declaraciones de Python, usamos cursiva para


describir los tipos de có digo que podrían aparecer en ese punto de un programa.
Por ejemplo, la expresió n booleana indica que cualquier expresió n que se evalú e
como Verdadero o Falso puede seguir a la palabra reservada si, y el bloque de
có digo indica que cualquier secuencia de declaraciones de Python puede seguir
a otro:.
Capitulo 2. Introducción a 1

Considere el siguiente programa que imprime "Par" si el valor de la variable x es par


e "Impar" en caso contrario:
si x%2 == 0:
imprime
'Even' más:
imprimir 'Impar'
imprimir 'Terminado con condicional'

La expresió n x%2 == 0 se evalú a como True cuando el resto de x dividido por 2 es 0, y


False en caso contrario. Recuerde que == se usa para comparació n, ya que = está
reservado para asignació n.

Sangríaes semá nticamente significativo en Python. Por ejemplo, si la ú ltima


declaració n en el có digo anterior tuviera sangría, sería parte del bloque de có digo
asociado con el else, en lugar del bloque de có digo que sigue a la declaració n
condicional.

Python es inusual en el uso de la sangría de esta manera. La mayoría de los otros


lenguajes de programació n usan algú n tipo de símbolos de paréntesis para delinear
bloques de có digo, por ejemplo, C encierra bloques entre llaves, { }. Una ventaja del
enfoque de Python es que asegura que la estructura visual de un programa sea una
representació n precisa de la estructura semá ntica de ese programa.

Cuando el bloque verdadero o el bloque falso de un condicional contienen otro


condicional, se dice que las declaraciones condicionales está n anidadas. En el
siguiente có digo, hay condicionales anidados en ambas ramas de la instrucció n if
de nivel superior.
si x%2 == 0:
si x%3 == 0:
imprime 'Divisible por 2 y
3' más:
imprime 'Divisible por 2 y no por
3' elif x%3 == 0:
imprime 'Divisible por 3 y no por 2'

El elif en el có digo anterior significa "else if".

A menudo es conveniente usar expresiones booleanas compuestas en la prueba de


un condicional, por ejemplo,
si x < y y x < z:
imprime 'x es
menor'
elif y < z:
imprime 'y es
menor' else:
imprime 'z es menor'

Los condicionales nos permiten escribir programas que son má s interesantes


que los programas de línea recta, pero la clase de programas de bifurcació n
todavía es bastante limitada. Una forma de pensar en el poder de una clase de
programas es en té rminos de cuá nto tiempo pueden tardar en ejecutarse.
Suponga que cada línea de có digo tarda una unidad de tiempo en ejecutarse. Si
un programa lineal tiene n líneas de có digo, tardará n unidades de tiempo en
ejecutarse. ¿Qué pasa con un programa de bifurcació n con n líneas de có digo?
Puede tardar menos de n unidades de tiempo en ejecutarse, pero no puede
tardar má s, ya que cada línea de có digo se ejecuta como má ximo una vez.
1 Capítulo 2. Introducción a

Se dice que un programa para el cual el tiempo má ximo de ejecució n está limitado
por la duració n del programa se ejecuta en tiempo constante. Esto no quiere decir
que cada vez que se ejecute ejecute el mismo nú mero de pasos. Significa que existe
una constante, k, tal que se garantiza que el programa no tomará má s de k pasos para
ejecutarse. Esto implica que el tiempo de ejecució n no crece con el tamañ o de la
entrada al programa.

Los programas de tiempo constante son bastante limitados en lo que pueden hacer.
Considere, por ejemplo, escribir un programa para contar los votos en una elecció n.
Sería realmente sorprendente si uno pudiera escribir un programa que pudiera
hacer esto en un tiempo que fuera independiente del nú mero de votos emitidos. De
hecho, se puede probar que es imposible hacerlo. El estudio de la dificultad
intrínseca de los problemas es el tema de la complejidad computacional.
Volveremos sobre este tema varias veces en este libro.

Afortunadamente, solo necesitamos una construcció n má s del lenguaje de


programació n, la iteració n, para poder escribir programas de complejidad
arbitraria. Llegamos a eso en la Secció n 2.4.

Ejercicio de dedos:Escriba un programa que examine tres variables, x, y y z, e


imprima el nú mero impar má s grande entre ellas. Si ninguno de ellos es impar,
debe imprimir un mensaje a tal efecto.

2.3 Cadenas y entrada


Los objetos de tipo str se utilizan para representar cadenas de caracteres.11 Literales de
tipo
str se puede escribir usando comillas simples o dobles, por
ejemplo, 'abc' o "abc". el literal'123' denota una cadena de
caracteres, no el número ciento veintitrés.

Intente escribir las siguientes expresiones en el intérprete de Python (recuerde que


>>> es un aviso, no algo que escriba):
>>> 'un'
>>> 3*4
>>> 3*'a'
>>> 3+4
>>> 'a'+'a'

Se dice que el operador + está sobrecargado: tiene diferentes significados segú n los
tipos de objetos a los que se aplica. Por ejemplo, significa suma cuando se aplica a
dos nú meros y concatenació n cuando se aplica a dos cadenas. El operador * tambié n
está sobrecargado. Significa lo que espera que signifique cuando sus operandos son
ambos nú meros. Cuando se aplica a un int y un str, duplica el str. Por ejemplo, la
expresió n 2*'Juan' tiene el valor

11A diferencia de muchos lenguajes de programació n, Python no tiene ningú n tipo


correspondiente a un cará cter. En su lugar, utiliza cadenas de longitud 1.
Capitulo 2. Introducción a 1

'Juan Juan'. Hay una lógica en esto. Así como la expresión3*2 es


equivalente a
2+2+2, la expresion3*'a'es equivalente a'a'+'a'+'a'.

Ahora intenta escribir


>>> un
>>> 'a'*'a'

Cada una de estas líneas genera un mensaje de

error. La primera línea produce el mensaje.


NameError: el nombre 'a' no está definido

Como a no es un literal de ningú n tipo, el inté rprete lo trata como un nombre.


Sin embargo, dado que ese nombre no está vinculado a ningú n objeto, intentar
usarlo provoca un error de tiempo de ejecució n.

El có digo 'a'*'a' produce el mensaje de error


TypeError: no se puede multiplicar la secuencia por no int del tipo 'str'

Que exista verificació n de tipo es algo bueno. Convierte los errores por descuido (ya
veces sutiles) en errores que detienen la ejecució n, en lugar de errores que hacen
que los programas se comporten de maneras misteriosas. La verificació n de tipos en
Python no es tan fuertecomo en algunos otros lenguajes de programació n (por ejemplo,
Java). Por ejemplo, está bastante claro qué debería significar < cuando se usa para comparar dos
cadenas o dos nú meros. Pero, ¿cuá l debería ser el valor de '4' < 3? De manera bastante
arbitraria, los diseñ adores de Python decidieron que debería ser Falso, porque todos los valores
numéricos deberían ser menores que todos los valores de tipo str. Los diseñ adores de algunos
otros lenguajes decidieron que dado que tales expresiones no tienen un significado obvio,
deberían generar un mensaje de error.

Las cadenas son uno de varios tipos de secuencias en Python. Comparten las
siguientes operaciones con todos los tipos de secuencia.

La longitud de una cadena se puede encontrar usando el Lenfunció n. Por ejemplo, el


valor delen('abc')es3.

Indexaciónse puede utilizar para extraer caracteres individuales de una cadena. En


Python, toda la indexació n se basa en cero. Por ejemplo, escribir 'abc'[0] en el
inté rprete hará que muestre la cadena 'a'. Escribir 'abc'[3] producirá el mensaje de
error IndexError: índice de cadena fuera de rango. Dado que Python usa 0 para
indicar el primer elemento de una cadena, se accede al ú ltimo elemento de una
cadena de longitud 3 usando el índice 2. Los nú meros negativos se usan para
indexar desde el final de una cadena. Por ejemplo, el valor de 'abc'[-1] es 'c'.

rebanarse utiliza para extraer subcadenas de longitud arbitraria. Si s es una


cadena, la expresió n s[start:end] denota la subcadena de s que comienza en el
inicio del índice y termina en el final del índice-1. Por ejemplo, 'abc'[1:3] = 'bc'. ¿Por
qué termina en el índice end-1 en lugar de end? De modo que expresiones como
'abc'[0:len('abc')] tienen el valor que cabría esperar. Si se omite el valor anterior a
los dos puntos, el valor predeterminado es 0. Si se omite el valor posterior a los dos
puntos, el valor predeterminado es la longitud de la cadena. En consecuencia, la
expresió n 'abc'[:] es semá nticamente equivalente a la má s prolija
'abc'[0:len('abc')].
1 Capítulo 2. Introducción a

2.3.1 Aporte
Python 2.7 tiene dos funciones (consulte el Capítulo 4 para ver una discusió n de las
funciones en Python) que se pueden usar para obtener entradas directamente de un
usuario, input y raw_input.12 Cada una toma una cadena como argumento y la
muestra como un aviso en el shell. . Luego espera a que el usuario escriba algo,
seguido de presionar la tecla Intro. Para raw_input, la línea de entrada se trata como
una cadena y se convierte en el valor devuelto por la funció n; input trata la línea
escrita como una expresió n de Python e infiere un tipo. En este libro, usamos solo
raw_input, que es menos probable que conduzca a programas que se comporten de
manera inesperada.

Considere el có digo
>>> nombre = raw_input('Ingrese su
nombre: ') Ingrese su nombre: George
Washington
>>> print '¿Eres realmente', nombre,
'?' ¿Eres realmente George Washington?
>>> print '¿De verdad eres ' + nombre +
'?' ¿Eres realmente George Washington?

Observe que la primera declaració n de impresió n introduce un espacio en blanco


antes del "?" Hace esto porque cuando print recibe mú ltiples argumentos, coloca un
espacio en blanco entre los valores asociados con los argumentos. La segunda
declaració n de impresió n usa la concatenació n para producir una cadena que no
contiene el espacio en blanco superfluo y lo pasa como el ú nico argumento para
imprimir.

Ahora considera,

>>> n = raw_input('Ingrese un int:


') Ingrese un int: 3
>>> tipo de impresión (n)
<escriba 'cadena'>

Observe que la variable n está ligada a la cadena '3', no al int 3. Así, por ejemplo, el
valor de la expresió n n*4 es '3333' en lugar de 12. La buena noticia es que cada vez
que una cadena es vá lida literal de algú n tipo, se le puede aplicar una conversió n de
tipo.

Tipo de conversiones(también llamados conversiones de tipo) se usan a menudo


en el có digo de Python. Usamos el nombre de un tipo para convertir valores a ese
tipo. Así, por ejemplo, el valor deint('3')*4es12. Cuando unflotarse convierte en
unEn t, el nú mero se trunca (no se redondea), por ejemplo, el valor de int(3.9)es
elentero 3.

2.4 Iteración
Un mecanismo de iteració n genérico (también llamado bucle) se muestra en la
Figura 2.4. Como una declaració n condicional, comienza con una prueba. Si la
prueba se evalú a como Verdadero, el programa ejecuta el cuerpo del ciclo una vez y
luego regresa para reevaluar la prueba. Este proceso se repite hasta que la prueba
se evalú a como Falso, despué s de lo cual el control pasa al có digo que sigue a la

declaració n de iteració n.

12Python3 tiene un solo comando, entrada. Algo confuso, la entrada de Python 3 tiene la
misma semá ntica que raw_input en Python 2.7. Imagínate.
Capitulo 2. Introducción a 1

Figura 2.4 Diagrama de flujo para la iteración

Considere el siguiente ejemplo:


# Elevar al cuadrado un entero,
de la manera difícil x = 3
ans = 0
itersLeft = x
while (itersLeft != 0):
ans = ans + x
itersLeft = itersLeft - 1
imprimir str(x) + '*' + str(x) + ' = ' + str(respuesta)

El có digo comienza vinculando la variable x al nú mero entero 3. Luego procede a


elevar x al cuadrado usando sumas repetitivas. La siguiente tabla muestra el valor
asociado a cada variable cada vez que se alcanza la prueba al inicio del ciclo. Lo
construimos simulando el có digo a mano, es decir, fingimos ser un inté rprete de
Python y ejecutamos el programa con lá piz y papel.
El uso de lá piz y papel puede parecer un poco extrañ o, pero es una forma excelente de
entender có mo se comporta un programa.13
prueba # X respu itersLeft
esta
1 3 0 3
2 3 3 2
3 3 6 1
4 3 9 0

La cuarta vez que se alcanza la prueba, se evalú a como Falso y el flujo de control
continú a con la instrucció n de impresió n que sigue al ciclo.

¿Para qué valores de x terminará este programa?

Si x == 0, el valor inicial de itersLeft tambié n será 0 y el cuerpo del ciclo nunca se


ejecutará . Si x > 0, el valor inicial de itersLeft será mayor que 0 y se ejecutará el cuerpo
del bucle.

13Tambié n es posible simular a mano un programa usando lá piz y papel, o incluso un


editor de texto.
2 Capítulo 2. Introducción a

Cada vez que se ejecuta el cuerpo del ciclo, el valor de itersLeft se reduce
exactamente en 1. Esto significa que si itersLeft comenzó mayor que 0, despué s de un
nú mero finito de iteraciones del ciclo, itersLeft == 0. En este punto, la prueba del
ciclo se evalú a como False y el control continú a con el có digo que sigue a la
instrucció n while.

¿Qué pasa si el valor de x es -1? Algo muy malo sucede. El control entrará en el bucle
y cada iteració n moverá itersLeft má s lejos de 0 en lugar de acercarse a é l. Por lo
tanto, el programa continuará ejecutando el ciclo para siempre (o hasta que ocurra
algo malo, por ejemplo, un error de desbordamiento). ¿Có mo podemos eliminar este
defecto en el programa? Inicializar itersLeft al valor absoluto de x casi funciona. El
ciclo termina, pero imprime un valor negativo. Si también se cambia la declaració n
de asignació n dentro del bucle, a ans = ans+abs(x), el có digo funciona
correctamente.

Ahora hemos cubierto prá cticamente todo lo que necesitamos saber sobre Python
para comenzar a escribir programas interesantes que se ocupen de nú meros y
cadenas. Ahora nos tomamos un breve descanso del aprendizaje del idioma. En el
pró ximo capítulo, usamos Python para resolver algunos problemas simples.

Ejercicio de dedos:Escriba un programa que le pida al usuario que ingrese 10


nú meros enteros y luego imprima el nú mero impar má s grande que se ingresó . Si
no se ingresó un nú mero impar, debe imprimir un mensaje a tal efecto.
3ALGUNOS PROGRAMAS NUMÉRICOS SENCILLOS

Ahora que hemos cubierto algunas construcciones bá sicas de Python, es hora de


comenzar a pensar en có mo podemos combinar esas construcciones para escribir
algunos programas simples. En el camino, introduciremos algunas
construcciones de lenguaje má s y algunas té cnicas algorítmicas.

3.1 Enumeración exhaustiva


El có digo de la figura 3.1 imprime la raíz cú bica entera, si existe, de
un entero. Si la entrada no es un cubo perfecto, imprime un mensaje
a tal efecto.

Figura 3.1 Uso de la enumeración exhaustiva para encontrar la


#Encontrar la raíz cúbica de un cubo
raíz perfecto
cúbica
x = int(raw_input('Ingrese un número
entero:
¿Para ')) ans
qué valores de =x terminará
0 este programa?
while ans**3 <
La respuesta
abs(x): es,ans
"todos
= los nú meros enteros". Esto se puede argumentar de manera
ans
bastante + 1
simple.
si respuesta**3 != abs(x):
 print
El valor
x,de'no
la expresió nrespuesta**3empieza a0, y se hace má s grande
es un cubo
perfecto' else:
cada vez que pasa por el bucle.
si x < 0:
 Cuando alcanza=o supera
respuesta abdominales(x), el ciclo termina.
- respuesta

 Desdeabdominales(x)siempre es positivo, solo hay un nú mero


finito de iteraciones antes de que el ciclo deba terminar.
Cada vez que escribe un bucle, debe pensar en un apropiado
función decreciente. Esta es una funció n que tiene las siguientes propiedades:

1. Mapea un conjunto de variables de programa en un nú mero entero.


2. Cuando se ingresa al bucle, su valor no es negativo.
3. Cuando su valor es <=0, el ciclo termina.
4. Su valor se reduce cada vez que pasa por el bucle.
¿Cuá l es la funció n decreciente del bucle de la figura 3.1? Es
abs(x) - respuesta**3.
2 Capítulo 3. Algunos programas numéricos

Ahora, insertemos algunos errores y veamos qué sucede. Primero, intente comentar
la declaració n ans = 0. El inté rprete de Python imprime el mensaje de error,
NameError: el nombre 'ans' no está definido, porque el inté rprete intenta encontrar
el valor al que está vinculado ans antes de que se haya vinculado a algo. Ahora,
restaure la inicializació n de ans, reemplace la declaració n ans = ans + 1 por
ans = ans, y trata de encontrar la raíz cúbica de 8. Después de que
te canses de esperar, ingresa "control c" (mantén presionada la tecla
control y la tecla c simultáneamente). Esto lo regresará al indicador
de usuario en el shell.

Ahora, agregue la declaració n


print 'Valor de la función decreciente abs(x) - ans**3 is',\
abs(x) - ans**3

al comienzo del bucle e intente ejecutarlo de nuevo. (La \ al final de la primera


línea de la declaració n de impresió n se usa para indicar que la declaració n continú a
en la siguiente línea).

Esta vez se imprimirá


El valor de la función decreciente abs(x) - ans**3 es 8

una y otra vez.

El programa se habría ejecutado para siempre porque el cuerpo del ciclo ya no


reduce la distancia entre ans**3 y abs(x). Cuando se enfrentan a un programa que
parece no terminar, los programadores experimentados a menudo insertan
sentencias de impresió n, como la que se muestra aquí, para comprobar si la funció n
decreciente realmente está siendo decrementada.

La técnica algorítmica utilizada en este programa es una variante de adivinar y


verificar llamada enumeració n exhaustiva. Enumeramos todas las posibilidades
hasta llegar a la respuesta correcta o agotar el espacio de posibilidades. A primera
vista, esto puede parecer una forma increíblemente estú pida de resolver un
problema. Sorprendentemente, sin embargo, los algoritmos de enumeració n
exhaustiva suelen ser la forma má s prá ctica de resolver un problema. Por lo general,
son fá ciles de implementar y fá ciles de entender. Y, en muchos casos, corren lo
suficientemente rá pido para todos los propó sitos prá cticos. Asegú rese de eliminar o
comentar la declaració n de impresió n que insertó y vuelva a insertar la declaració n
ans = ans + 1, y luego intente encontrar la raíz cú bica de 1957816251. Parecerá que
el programa finaliza casi instantá neamente. Ahora, prueba 7406961012236344616.

Como puede ver, incluso si se requieren millones de conjeturas, no suele ser un


problema. Las computadoras modernas son asombrosamente rá pidas. Se necesita
del orden de un nanosegundo, una milmillonésima de segundo, para ejecutar una
instrucció n. Es un poco difícil apreciar lo rá pido que es. En perspectiva, la luz tarda
un poco má s de un nanosegundo en recorrer un solo pie (0,3 metros). Otra forma
de pensar en esto es que en el tiempo que tarda el sonido de su voz en viajar cien
pies, una computadora moderna puede ejecutar millones de instrucciones.
Capítulo 3. Algunos programas numéricos 2

Solo por diversió n, intente ejecutar el có digo


max = int(raw_input('Ingrese un entero positivo:
')) i = 0
mientras yo <
max: yo =
yo + 1
imprimir yo

Vea qué tan grande es el nú mero entero que necesita ingresar antes de que haya
una pausa perceptible antes de que se imprima el resultado.

Ejercicio de dedos:Escriba un programa que le pida al usuario que ingrese un


nú mero entero e imprima dos nú meros enteros, root y pwr, de modo que 0 < pwr <
6 y root**pwr sea igual al nú mero entero ingresado por el usuario. Si no existe tal
par de enteros, debe imprimir un mensaje a tal efecto.

3.2 Para bucles


Los bucles while que hemos usado hasta ahora está n muy estilizados. Cada uno
itera sobre una secuencia de enteros. Python proporciona un mecanismo de
lenguaje, el bucle for, que se puede utilizar para simplificar los programas que
contienen este tipo de iteraciones.

La forma general de una instrucció n for es (recuerde que las palabras en


cursiva son descripciones de lo que puede aparecer, no có digo real):
paravariableensecuencia:blo
que de código

La variable siguiente a está vinculada al primer valor de la secuencia y se ejecuta el


bloque de có digo. Luego, a la variable se le asigna el segundo valor en la secuencia y
el bloque de có digo se ejecuta nuevamente. El proceso continú a hasta que se agota la
secuencia o se ejecuta una declaració n de interrupció n dentro del bloque de có digo.

La secuencia de valores vinculados a la variable se genera má s comú nmente


utilizando el rango de funció n incorporado, que devuelve una secuencia que contiene
una progresió n aritmética. La funció n de rango toma tres argumentos enteros: inicio,
parada y paso. Produce el inicio de progresió n, inicio + paso, inicio + 2*paso, etc.
Si step es positivo, el ú ltimo elemento es el entero má s grande start + i*step less than
stop. Si el paso es negativo, el ú ltimo elemento es el entero má s pequeñ o
inicio + i*pasomas grande quedetener. Por ejemplo,rango (5, 40, 10)produce
la secuencia[5, 15, 25, 35], yrango (40, 5, -10)produce la secuencia
[40, 30, 20, 10]. Si se omite el primer argumento, el valor predeterminado es 0, y
si se omite el ú ltimo argumento (el tamañ o del paso), el valor predeterminado es1.
Por ejemplo,rango (0, 3)yrango(3)ambos producen la secuencia[0, 1, 2].

Con menos frecuencia, especificamos la secuencia que se repetirá en un bucle for


mediante el uso de un literal, por ejemplo, [0, 1, 2]. En Python 2.7, range genera la
secuencia completa cuando se invoca. Por lo tanto, por ejemplo, la expresió n
range(1000000) usa bastante memoria. Esto se puede evitar usando el
2 Capítulo 3. Algunos programas numéricos

funció n incorporadarango xen lugar derango, desderango xgenera los valores solo a
medida que los necesita elparabucle.14

Considere el có digo

x = 4
para i en el rango
(0, x): imprime i

se imprime
0
1
2
3

Ahora, piensa en el có digo.

x = 4
para i en el rango
(0, x): imprime i
x = 5

Plantea la cuestió n de si cambiar el valor de x dentro del ciclo afecta el nú mero de


iteraciones. No es asi. La funció n de rango en la línea con for se evalú a justo antes de
la primera iteració n del bucle y no se vuelve a evaluar para iteraciones posteriores.
Para ver có mo funciona esto, considere
x = 4
para j en rango(x):
para i en
rango(x):
imprimi
r ix =
2

se imprime
0
1
2
3
0
1
0
1
0
1

porque la funció n de rango en el ciclo externo se evalú a solo una vez, pero la
funció n de rango en el ciclo interno se evalú a cada vez que se alcanza la instrucció n
for interna.

El có digo de la Figura 3.2 vuelve a implementar el algoritmo de enumeració n


exhaustiva para encontrar raíces cú bicas. La instrucció n break en el bucle for hace
que el bucle termine antes de que se haya ejecutado en cada elemento de la
secuencia sobre la que está iterando. Cuando se ejecuta, una declaració n de ruptura
sale del ciclo má s interno en el que está encerrada.

14En Python 3, range se comporta de la misma manera que xrange en Python 2.


Capítulo 3. Algunos programas numéricos 2

#Encontrar la raíz cúbica de un cubo perfecto


x = int(raw_input('Ingrese un número
entero: ')) for ans in range(0, abs(x)
+1):
si ans**3 >=
abs(x): romper
si respuesta**3 != abs(x):
print x, 'no es un cubo
perfecto' else:
si x < 0:
respuesta = - respuesta

Figura 3.2 Usoparayromperdeclaraciones

La declaración for se puede usar para iterar convenientemente sobre


los caracteres de una cadena. Por ejemplo,

totales = 0
para c en '123456789':
total = total + int(c)
imprimir total

suma los dígitos en la cadena indicada por el literal '123456789' e imprime el total.

Ejercicio de dedos:Dejarsser una cadena que contiene una secuencia de nú meros


decimales separados por comas, por ejemplo, s = '1.23, 2.4, 3.123'. Escriba
un programa que imprima la suma de los nú meros ens.

3.3 Soluciones aproximadas y búsqueda de bisección


Imagina que alguien te pide que escribas un programa que encuentre la raíz
cuadrada de cualquier nú mero no negativo. ¿Qué debes hacer?

Probablemente debería comenzar diciendo que necesita una mejor declaració n del
problema. Por ejemplo, ¿qué debería hacer el programa si se le pide que encuentre
la raíz cuadrada de 2? La raíz cuadrada de 2 no es un nú mero racional. Esto
significa que no hay forma de representar con precisió n su valor como una cadena
finita de dígitos (o como un flotante), por lo que el problema planteado
inicialmente no se puede resolver.

Lo que se debe haber pedido es un programa que encuentre una aproximació n a la


raíz cuadrada, es decir, una respuesta que se acerque lo suficiente a la raíz cuadrada
real para que sea ú til. Volveremos a este tema con considerable detalle má s adelante
en el libro.
Pero por ahora, pensemos en "lo suficientemente cerca" como una respuesta
que se encuentra dentro de alguna constante, llá mese é psilon, de la respuesta
real.

El có digo de la figura 3.3 implementa un algoritmo que encuentra una


aproximació n a una raíz cuadrada. Utiliza un operador, +=, que no hemos utilizado
previamente. El có digo ans += step es semá nticamente equivalente al có digo má s
detallado
ans = ans+paso. los operadores-=y*=trabajar de manera similar.
2 Capítulo 3. Algunos programas numéricos

X = 25
épsilon = 0.01
paso =
épsilon**2
numGuesses = 0
respuesta = 0.0
while abs(ans**2 - x) >= épsilon y ans <= x:
ans += paso
númeroAdivinas += 1
imprime 'númConjeturas =',
númeroConjeturas si abs(ans**2
- x) >= épsilon:
print 'Error en la raíz cuadrada
de', x else:
Figura 3.3 Aproximación a la raíz cuadrada mediante enumeración exhaustiva

Una vez má s, estamos utilizando la enumeració n exhaustiva. Ten en cuenta que


este método para encontrar la raíz cuadrada no tiene nada en comú n con la
forma de encontrar raíces cuadradas con un lá piz que quizá s hayas aprendido en
la escuela secundaria. A menudo ocurre que la mejor manera de resolver un
problema con una computadora es bastante diferente de có mo se abordaría el
problema a mano.

Cuando se ejecuta el có digo, se imprime


númeroConjeturas = 49990
4.999 está cerca de la raíz cuadrada de 25

¿Deberíamos estar desilusionados de que el programa no se diera cuenta de que 25


es un cuadrado perfecto e imprimiera 5? No. El programa hizo lo que estaba
destinado a hacer. Aunque hubiera estado bien imprimir 5, hacerlo no es mejor que
imprimir cualquier valor lo suficientemente cercano a 5.

¿Qué crees que sucederá si establecemos x = 0.25? ¿Encontrará una raíz cercana a
0,5? No. La enumeració n exhaustiva es una técnica de bú squeda que funciona solo si
el conjunto de valores buscados incluye la respuesta. En este caso, estamos
enumerando los valores entre 0 y x. Cuando x está entre 0 y 1, la raíz cuadrada de x
no se encuentra en este intervalo. Una forma de solucionar esto es cambiar la
primera línea del ciclo while a
while abs(ans**2 - x) >= épsilon y ans*ans <= x:

Ahora, pensemos en cuá nto tiempo tardará en ejecutarse el programa. El nú mero


de iteraciones depende de qué tan cerca esté la respuesta de cero y del tamañ o de
los pasos. En términos generales, el programa ejecutará el ciclo while en la
mayoría de los tiempos x/paso.

Probemos el có digo en algo má s grande, por ejemplo, x = 123456. Se ejecutará un


poco y luego imprimirá
numGuesses = 3513631
Error en la raíz cuadrada de 123456

¿Qué crees que pasó ? Seguramente existe un nú mero de punto flotante que se
aproxima a la raíz cuadrada de 123456 con una precisió n de 0,01. ¿Por qué nuestro
programa no lo encontró ? El problema es que nuestro tamañ o de paso era
demasiado grande y el programa omitió todas las respuestas adecuadas. Intente
hacer que el paso sea igual a epsilon ** 3 y
Capítulo 3. Algunos programas numéricos 2

ejecutando el programa. Eventualmente encontrará una respuesta adecuada, pero


es posible que no tenga la paciencia para esperar a que lo haga.

Aproximadamente, ¿cuá ntas conjeturas tendrá que hacer? El tamañ o del paso será
0,000001 y la raíz cuadrada de 123456 es de alrededor de 351,36. Esto significa que
el programa tendrá que hacer alrededor de 351 000 000 intentos para encontrar una
respuesta satisfactoria. Podríamos intentar acelerarlo comenzando má s cerca de la
respuesta, pero eso supone que sabemos la respuesta.

Ha llegado el momento de buscar una forma diferente de atacar el problema.


Necesitamos elegir un mejor algoritmo en lugar de ajustar el actual. Pero antes de
hacerlo, veamos un problema que, a primera vista, parece ser completamente
diferente a la bú squeda de raíces.

Considere el problema de descubrir si una palabra que comienza con una secuencia
dada de letras aparece en algú n diccionario impreso del idioma inglés. La
enumeració n exhaustiva, en principio, funcionaría. Puede comenzar en la primera
palabra y examinar cada palabra hasta que encuentre una palabra que comience con
la secuencia de letras o se quede sin palabras para examinar. Si el diccionario
contiene n palabras, tomaría, en promedio, n/2 sondeos para encontrar la palabra. Si
la palabra no estuviera en el diccionario, necesitaría n sondeos. Por supuesto,
aquellos que han tenido el placer de buscar una palabra en un diccionario físico (en
lugar de en línea) nunca procederían de esta manera.

Afortunadamente, la gente que publica diccionarios se toma la molestia de poner las


palabras en orden lexicográ fico. Esto nos permite abrir el libro en una pá gina donde
pensamos que podría estar la palabra (p. ej., cerca del medio para palabras que
comienzan con la letra m). Si la secuencia de letras precede lexicográ ficamente a la
primera palabra de la pá gina, sabemos que retrocedemos. Si la secuencia de letras
sigue a la ú ltima palabra de la pá gina, sabemos que debemos avanzar. De lo
contrario, verificamos si la secuencia de letras coincide con una palabra en la
pá gina.

Ahora tomemos la misma idea y apliqué mosla al problema de encontrar la raíz


cuadrada de x. Supongamos que sabemos que una buena aproximació n a la raíz
cuadrada de x se encuentra entre 0 y má x. Podemos aprovechar el hecho de que los
nú meros está n totalmente ordenados. Es decir, para cualquier par de nú meros
distintos, n1 y n2, ya sea
n1 < n2 o n1 > n2. Entonces, podemos pensar que la raíz cuadrada de x está en algún lugar de la
línea

0 máximo

y empezar a buscar ese intervalo. Como no sabemos necesariamente por dó nde


empezar a buscar, empecemos por el medio.

0 adivinar máximo

Si esa no es la respuesta correcta (y no lo será la mayor parte del tiempo), pregunte


si es demasiado grande o demasiado pequeñ a. Si es demasiado grande, sabemos que
la respuesta debe estar a la izquierda. Si es demasiado pequeñ o, sabemos que la
respuesta debe estar a la derecha. Luego repetimos el proceso en el intervalo má s
pequeñ o. La Figura 3.4 contiene una implementació n y prueba de este algoritmo.
2 Capítulo 3. Algunos programas numéricos

X = 25
épsilon = 0,01
número de conjeturas = 0
bajo = 0.0
alto = máximo (1.0, x)
respuesta = (alto + bajo)/2.0
while abs(ans**2 - x) >= épsilon:
imprimir 'bajo =', bajo, 'alto =', alto, 'ans =', ans
numGuesses += 1
si
respuest
a**2 <
x: baja
=
respuest
a

Figura 3.4 Usando la búsqueda de bisección para aproximar la


raíz cuadrada

Cuando se ejecuta, imprime


bajo = 0,0 alto = 25 respuesta = 12,5
bajo = 0,0 alto = 12,5 y = 6,25
bajo = 0,0 alto = 6,25 y = 3,125
bajo = 3,125 alto = 6,25 y = 4,6875
bajo = 4,6875 alto = 6,25 y = 5,46875
bajo = 4,6875 alto = 5,46875 y = 5,078125
bajo = 4,6875 alto = 5,078125 y = 4,8828125
bajo = 4,8828125 alto = 5,078125 y = 4,98046875
bajo = 4,98046875 alto = 5,078125 y = 5,029296875
bajo = 4,98046875 alto = 5,029296875 y = 5,0048828125
bajo = 4,98046875 alto = 5,0048828125 y = 4,99267578125
bajo = 4,99267578125 alto = 5,0048828125 y = 4,99877929688
bajo = 4,99877929688 alto = 5,0048828125 y = 5,00183105469
número de adivinanzas = 13
5.00030517578 está cerca de la raíz cuadrada de 25

Tenga en cuenta que encuentra una respuesta diferente a nuestro algoritmo


anterior. Eso está perfectamente bien, ya que aú n cumple con el enunciado del
problema.

Má s importante aú n, observe que en cada iteració n el tamañ o del espacio a


buscar se reduce a la mitad. Debido a que divide el espacio de bú squeda por la
mitad en cada paso, se denomina bú squeda de bisecció n. La bú squeda de
bisecció n es una gran mejora con respecto a nuestro algoritmo anterior, que
reducía el espacio de bú squeda solo una pequeñ a cantidad en cada iteració n.

Intentemos x = 123456 nuevamente. Esta vez, el programa solo necesita treinta


intentos para encontrar una respuesta aceptable. ¿Qué tal x = 123456789? Solo se
necesitan cuarenta y cinco conjeturas.

No hay nada especial en el hecho de que estemos usando este algoritmo para
encontrar raíces cuadradas. Por ejemplo, al cambiar un par de 2 a 3, podemos
usarlo para aproximar una raíz cú bica de un nú mero no negativo. En el pró ximo
capítulo presentaremos un mecanismo de lenguaje que nos permite generalizar
este có digo para encontrar cualquier raíz.

Ejercicio de dedos:¿Qué haría el có digo de la figura 3.4 si la declaració nX = 25


fueron reemplazados porx = -25?
Capítulo 3. Algunos programas numéricos 2

Ejercicio de dedos:¿Qué tendría que cambiarse para hacer el có digo en la Figura


3.4 trabajo para encontrar una aproximació n a la raíz cú bica de nú meros
negativos y positivos? (Pista: piensa en cambiar bajopara asegurarse de que la
respuesta se encuentra dentro de la regió n que se está buscando).

3.4 Algunas palabras sobre el uso de flotadores


La mayoría de las veces, los nú meros de tipo flotante proporcionan una
aproximació n razonablemente buena a los nú meros reales. Pero “la mayor parte del
tiempo” no es todo el tiempo, y cuando no es así, puede tener consecuencias
sorprendentes. Por ejemplo, intente ejecutar el có digo
x = 0,0
para i en el rango
(10): x = x +
0.1
si x == 1.0:
imprime x, '=
1.0' más:
imprime x, 'no es 1.0'

Tal vez usted, como la mayoría de las personas, encuentre doblemente sorprendente
que imprima,
1.0 no es 1.0

¿Por qué llega a la clá usula else en primer lugar? Y si de alguna manera llega allí,
¿por qué está imprimiendo una frase tan sin sentido?

Para comprender por qué sucede esto, debemos comprender có mo se representan


los nú meros de coma flotante en la computadora durante un cá lculo. Para entender
eso, necesitamos entender los nú meros binarios.

Cuando aprendió por primera vez sobre los nú meros decimales, es decir, los
nú meros en base 10, aprendió que un nú mero decimal está representado por una
secuencia de dígitos 0123456789. El dígito má s a la derecha es el lugar 100, el
siguiente dígito a la izquierda el lugar 101, etc. Por ejemplo, la secuencia de dígitos
decimales 302 representa 3*100 + 0*10 + 2*1. ¿Cuá ntos nú meros diferentes se
pueden representar mediante una secuencia de longitud n? Una secuencia de
longitud uno puede representar cualquiera de diez nú meros (0 - 9). Una secuencia
de longitud dos puede representar cien nú meros diferentes (0-99). Má s
generalmente, con una secuencia de longitud n, uno puede representar 10n
nú meros diferentes.

Los nú meros binarios, nú meros de base 2, funcionan de manera similar. Un nú mero


binario está representado por una secuencia de dígitos, cada uno de los cuales es 0 o
1. Estos dígitos a menudo se denominan bits. El dígito má s a la derecha es el lugar
20, el siguiente dígito a la izquierda el lugar 21, etc. Por ejemplo, la secuencia de
dígitos binarios 101 representa 1*4 + 0*2 + 1*1 = 5. ¿Cuá ntos nú meros diferentes se
pueden representada por una secuencia de longitud n? 2n.

Ejercicio de dedos:¿Cuá l es el equivalente decimal del nú mero binario?


10011?
3 Capítulo 3. Algunos programas numéricos

Tal vez porque la mayoría de la gente tiene diez dedos, parece que nos gusta usar
decimales para representar nú meros. Por otro lado, todos los sistemas informá ticos
modernos representan nú meros en binario. Esto no se debe a que las computadoras
nazcan con dos dedos. Esto se debe a que es fá cil construir interruptores de
hardware, es decir, dispositivos que pueden estar en solo uno de dos estados,
encendido o apagado. Que la computadora use una representació n binaria y las
personas una representació n decimal puede llevar a una disonancia cognitiva
ocasional.

En los lenguajes de programació n casi modernos, los nú meros no enteros se


implementan mediante una representació n llamada punto flotante. Por el
momento, supongamos que la representació n interna es en decimal.
Representaríamos un nú mero como un par de enteros: los dígitos significativos del
nú mero y un exponente. Por ejemplo, el nú mero 1,949 se representaría como el
par (1949, -3), que representa el producto 1949 X 10-3.

El nú mero de dígitos significativos determina la precisió n con la que se pueden


representar los nú meros. Si por ejemplo, solo hubiera dos dígitos significativos, el
nú mero 1.949 no se podría representar exactamente. Tendría que convertirse a
alguna aproximació n de 1.949, en este caso 1.9. Esa aproximació n se llama el valor
redondeado.

Las computadoras modernas usan representaciones binarias, no decimales.


Representamos los dígitos significativos y los exponentes en binario en lugar de
decimal y elevamos 2 en lugar de 10 al exponente. Por ejemplo, el nú mero 0,625
(5/8) se representaría como el par (101, -11); como 5/8 es 0,101 en binario y -11 es
la representació n binaria de -3, el par (101, -11) representa 5 X 2-3 = 5/8 = 0,625.

¿Qué pasa con la fracció n decimal 1/10, que escribimos en Python como 0.1? Lo
mejor que podemos hacer con cuatro dígitos binarios significativos es (0011, -101).
Esto es equivalente a 3/32, es decir, 0,09375. Si tuviéramos cinco dígitos binarios
significativos, representaríamos 0,1 como (11001, -1000), que equivale a 25/256, es
decir, 0,09765625. ¿Cuá ntos dígitos significativos necesitaríamos para obtener una
representació n de coma flotante exacta de 0.1? ¡Un nú mero infinito de dígitos! No
existen enteros sig y exp tales que sig * 2-exp sea igual a 0,1. Así que no importa
cuá ntos bits elija usar Python (o cualquier otro lenguaje) para representar nú meros
de punto flotante, solo podrá representar una aproximació n a 0.1. En la mayoría de
las implementaciones de Python, hay 53 bits de precisió n disponibles para nú meros
de coma flotante, por lo que los dígitos significativos almacenados para el nú mero
decimal 0.1 será n
11001100110011001100110011001100110011001100110011001

Esto es equivalente al nú mero decimal


0.1000000000000000055511151231257827021181583404541015625

Bastante cerca de 1/10, pero no exactamente 1/10.


Capítulo 3. Algunos programas numéricos 3

Volviendo al misterio original, ¿por qué


x = 0,0
para i en el rango
(10): x = x +
0.1
si x == 1.0:
imprime x, '=
1.0' más:
imprime x, 'no es 1.0'

imprimir
1.0 no es 1.0

Ahora vemos que la prueba x == 1.0 produce el resultado Falso porque el valor al que
está vinculada x no es exactamente 1.0. ¿Qué se imprime si agregamos al final de la
clá usula else el có digo print x == 10.0*0.1? Imprime Falso porque durante al menos una
iteració n del bucle, Python se quedó sin dígitos significativos e hizo algunos redondeos.
No es lo que nos enseñ aron nuestros maestros de primaria, pero sumar 0,1 diez veces
no produce el mismo valor que multiplicar 0,1 por 10.

Finalmente, ¿por qué el có digo

imprimir x

imprima 1.0 en lugar del valor real de la variable x? Porque los diseñ adores de
Python pensaron que sería conveniente para los usuarios si la impresió n hiciera un
redondeo automá tico. Esta es probablemente una suposició n precisa la mayor parte
del tiempo. Sin embargo, es importante tener en cuenta que lo que se muestra no
necesariamente coincide exactamente con el valor almacenado en la má quina.

Por cierto, si desea redondear explícitamente un nú mero de coma flotante, use la


funció n de redondeo. La expresió n round(x, numDigits) devuelve el nú mero de
punto flotante equivalente a redondear el valor de x a numDigits dígitos decimales
que siguen al punto decimal. Por ejemplo, print round(2**0.5, 3) imprimirá 1.414
como una aproximació n a la raíz cuadrada de 2.

¿Realmente importa la diferencia entre nú meros reales y de punto flotante? La


mayoría de las veces, afortunadamente, no es así. Sin embargo, una cosa por la que
casi siempre vale la pena preocuparse son las pruebas de igualdad. Como hemos
visto, usar == para comparar dos valores de punto flotante puede producir un
resultado sorprendente. Casi siempre es má s apropiado preguntar si dos valores
de punto flotante está n lo suficientemente cerca uno del otro, no si son idé nticos.
Entonces, por ejemplo, es mejor escribir abs(xy) < 0.0001 en lugar de x == y.

Otra cosa de la que preocuparse es la acumulació n de errores de redondeo. La


mayoría de las veces las cosas salen bien porque a veces el nú mero almacenado
en la computadora es un poco má s grande de lo previsto y, a veces, es un poco
má s pequeñ o de lo previsto. Sin embargo, en algunos programas, todos los
errores estará n en la misma direcció n y se acumulará n con el tiempo.
3 Capítulo 3. Algunos programas numéricos

3.5 Newton-Raphson
El algoritmo de aproximació n má s utilizado suele atribuirse a Isaac Newton. Por
lo general, se le llama método de Newton, pero a veces se le conoce como el
método de Newton-Raphson.15 Puede usarse para encontrar las raíces reales de
muchas funciones, pero lo veremos solo en el contexto de encontrar las raíces
reales de una funció n. polinomio de una variable. La generalizació n a polinomios
con mú ltiples variables es sencilla tanto matemá tica como algorítmicamente.

Un polinomio con una variable (por convenció n, escribiremos la variable como x) es


cero o la suma de un nú mero finito de términos distintos de cero, por ejemplo, 3x2 +
2x + 3.
Cada término, por ejemplo, 3x2, consta de una constante (el coeficiente del té rmino,
3 en este caso) multiplicada por la variable (x en este caso) elevada a un exponente
entero no negativo (2 en este caso). El exponente de una variable en un té rmino se
llama el grado de ese té rmino. El grado de un polinomio es el mayor grado de
cualquier término individual. Algunos ejemplos son 3 (grado 0), 2,5x + 12 (grado 1)
y 3x2 (grado 2). Por el contrario, 2/x y x0.5 no son polinomios.

Si p es un polinomio y ra un nú mero real, escribiremos p(r) para representar el valor


del polinomio cuando x = r. Una raíz del polinomio p es una solució n a la ecuació n p =
0, es decir, una r tal que p(r) = 0. Así, por ejemplo, el problema de encontrar una
aproximació n a la raíz cuadrada de 24 se puede formular como encontrar una x tal
que x2 – 24 ≈ 0.

Newton demostró un teorema que implica que si un valor, llá mese suposició n, es
una aproximació n a la raíz de un polinomio, entonces suponga:
p(suposició n)/p'(suposició n), donde p' es la primera derivada de p, es una mejor
aproximació n.16

Para cualquier constante k y cualquier coeficiente c, la primera derivada de cx2 + k


es 2cx. Por ejemplo, la primera derivada de x2 – k es 2x. Por lo tanto, sabemos que
podemos mejorar la suposició n actual, llá mela y, eligiendo como nuestra pró xima
suposició n
y - (y2 - k)/2y. Esto se llama aproximació n sucesiva. La figura 3.5 contiene có digo que
ilustra có mo usar esta idea para encontrar rá pidamente una aproximació n a la raíz
cuadrada.

15Joseph Raphson publicó un mé todo similar casi al mismo tiempo que Newton.
diecisé isSe
puede pensar que la primera derivada de una funció n f(x) expresa có mo cambia el
valor de f(x) con respecto a los cambios en x. Si no ha encontrado derivados anteriormente,
no se preocupe. No es necesario que los comprenda, ni tampoco los polinomios, para
comprender la implementació n del método de Newton.
Capítulo 3. Algunos programas numéricos 3

#Newton-Raphson para raíz cuadrada


#Encontrar x tal que x**2 - 24 está dentro de
epsilon de 0 epsilon = 0.01
k = 24,0
adivinar = k/2.0
while abs(adivinar*adivinar - k) >= épsilon:
adivinar = adivinar - (((adivinar**2) -
k)/(2*adivinar)) print 'Raíz cuadrada de', k, 'se
trata de', adivinar
Figura 3.5 Método de Newton-Raphson

Ejercicio de dedos:Agregue algo de có digo a la implementació n de Newton-


Raphson que realice un seguimiento del nú mero de iteraciones utilizadas para
encontrar la raíz. Use ese có digo como parte de un programa que compare la
eficiencia de Newton-Raphson y la bú squeda de bisecció n. (Debería descubrir que
Newton-Raphson es má s eficiente).
4FUNCIONES, ALCANCE Y ABSTRACCIÓN

Hasta ahora, hemos introducido construcciones de nú meros, asignaciones,


entrada/salida, comparaciones y bucles. ¿Qué tan poderoso es este subconjunto de
Python? En un sentido teó rico, es tan poderoso como lo necesitará s. Tales lenguajes
se llaman Turing completo. Esto significa que si un problema se puede resolver
mediante cá lculo, se puede resolver usando solo las declaraciones que ya ha visto.

Lo que no quiere decir que debas usar solo estas declaraciones. En este punto,
hemos cubierto una gran cantidad de mecanismos del lenguaje, pero el có digo
ha sido una sola secuencia de instrucciones, todas fusionadas. Por ejemplo, en el
ú ltimo capítulo vimos el có digo de la Figura 4.1.

Figura 4.1 Uso de la búsqueda de bisección para aproximar la raíz


X = 25 cuadrada
épsilon = 0,01
Esta es unanúmero
pieza dede
có conjeturas = 0pero carece de utilidad general. Funciona solo
digo razonable,
para valores bajo = 0.0 por las variables x y epsilon. Esto significa que si queremos
indicados
alto = max(1.0, x) y
reutilizarlo,=debemos
(alto + copiar el có digo, posiblemente editar los nombres de las
bajo)/2.0
variables y while
pegarloabs(ans**2
donde queramos.
- x) >=Debido a esto, no podemos usar fá cilmente
epsilon: numGuesses
este cá lculo dentro de algú n otro cá lculo+=má1 s complejo.
si
Ademá s, si queremosrespuest
calcular raíces cú bicas en lugar de raíces cuadradas, tenemos
a**2 <
que editar el có digo. Si queremos un programa que calcule raíces cuadradas y
x: baja
cú bicas (o, en realidad,
= raíces cuadradas en dos lugares diferentes), el programa
contendría mú ltiplesrespuest
fragmentos de có digo casi idé ntico. Esto es algo muy malo.
a
Cuanto má s có digo contiene un programa, má s posibilidades hay de que algo salga
mal y má s difícil es mantener el có digo. Imagine, por ejemplo, que hubo un error en
la implementació n inicial de la raíz cuadrada y que el error salió a la luz al probar
el programa. Sería demasiado fá cil arreglar la implementació n de la raíz cuadrada
en un lugar y olvidar que había un có digo similar en otro lugar que también
necesitaba reparació n.

Python proporciona varias características lingü ísticas que hacen que sea
relativamente fá cil generalizar y reutilizar el có digo. Lo má s importante es la
funció n.
Capter4.Divertidoacciones,Salbardilla,undABtr 3

4.1 Funciones y Alcance


Ya hemos utilizado una serie de funciones integradas, por ejemplo, max y abs en la Figura
4.1. La capacidad de los programadores para definir y luego usar sus propias
funciones, como si estuvieran integradas, es un salto cualitativo en conveniencia.

4.1.1 Definiciones de funciones


En Python, cada definició n de funció n tiene la forma17

definitivamentenombre de la función(lista de
parámetros formales):cuerpo de función

Por ejemplo, podríamos definir la funció n max18 por el có digo


def max(x, y):
si x > y:
volver x
más:
volver y
def es una palabra reservada que le dice a Python que una función
está a punto de ser definida. El nombre de la función (max en este
ejemplo) es simplemente un nombre que se usa para referirse a la
función.

La secuencia de nombres (X,yen este ejemplo) dentro de los paré ntesis que siguen
al nombre de la funció n se encuentran los pará metros formales de la funció n.
Cuando se usa la funció n, los pará metros formales está n vinculados (como en una
declaració n de asignació n) a los pará metros reales (a menudo denominados
argumentos) de la invocació n de la funció n (también denominada llamada de
funció n). Por ejemplo, la invocació n
máx(3, 4)

uneXa3yya4.

El cuerpo de la funció n es cualquier pieza de có digo de Python. Sin embargo, existe


una instrucció n especial, return, que solo se puede usar dentro del cuerpo de una
funció n.
Una llamada de funció n es una expresió n y, como todas las expresiones, tiene un
valor. Ese valor es el valor devuelto por la funció n invocada. Por ejemplo, el valor
de la expresió n max(3,4)*max(3,2) es 12, porque la primera invocació n de max
devuelve el int 4 y la segunda devuelve el int 3. Tenga en cuenta que la ejecució n
de una declaració n de devolució n termina esa invocació n de la funció n.

Para recapitular, cuando se llama a una funció n

1. Las expresiones que componen los pará metros reales se evalú an y los
pará metros formales de la funció n se vinculan a los valores resultantes. Por
ejemplo, la invocació nmáx(3+4, z)unirá el pará metro formalXa7y el
pará metro formalya cualquier valor de la variableztiene cuando se evalú a la
invocació n.

17Recuerde que la cursiva se usa para describir el có digo de Python.


18En la prá ctica, probablemente usaría la funció n incorporada max, en lugar de definir la
suya propia.
3 Capter4.Divertidoacciones,Salbardilla,undABtr

2. El punto de ejecució n (la siguiente instrucció n a ejecutar) se mueve desde el


punto de invocació n a la primera declaració n en el cuerpo de la funció n.
3. El có digo en el cuerpo de la funció n se ejecuta hasta que sedevolverse
encuentra la declaració n, en cuyo caso el valor de la expresió n que sigue
a ladevolverse convierte en el valor de la invocació n de la funció n, o no hay
má s instrucciones para ejecutar, en cuyo caso la funció n devuelve el
valorNinguno. (Si ninguna expresió n sigue a ladevolver, el valor de la
invocació n esNinguno.)
4. El valor de la invocació n es el valor devuelto.
5. El punto de ejecució n se vuelve a transferir al có digo inmediatamente
después de la invocació n.
Los pará metros proporcionan algo llamado abstracció n lambda,19 que permite a los
programadores escribir có digo que no manipula objetos específicos, sino cualquier
objeto que la persona que llama a la funció n elija usar como pará metros reales.

Ejercicio de dedos:Escriba una funció n isIn que acepte dos cadenas como
argumentos y devuelva True si cualquiera de las cadenas aparece en cualquier
parte de la otra, y False en caso contrario. Sugerencia: es posible que desee
utilizar la operació n str incorporada en.

4.1.2 Argumentos de palabras clave y valores predeterminados


En Python, hay dos formas en que los pará metros formales se vinculan a los
pará metros reales. El método má s comú n, que es el ú nico que hemos usado hasta
ahora, se llama posicional: el primer pará metro formal está vinculado al primer
pará metro real, el segundo formal al segundo real, etc. Python tambié n admite lo
que llama argumentos de palabras clave. , en el que los formales está n vinculados a
los reales usando el nombre del pará metro formal. Considere la definició n de la
funció n en la Figura 4.2. La funció n printName asume que firstName y lastName son
cadenas y que reverse es un valor booleano. Si reverse == True, imprime lastName,
firstName, de lo contrario, imprime firstName lastName.

Figura 4.2 Función que imprime un nombre


def printName(firstName, lastName, reverse):
if reverse:
Cada uno de los siguientes es una invocació n equivalente de printName:
imprimir apellido + ', ' +
printName('Olga', nombre más:
'Puchmajerova', False)
imprimir nombre,False)
printName('Olga', 'Puchmajerova', apellido
printName('Olga', 'Puchmajerova', reverse = False)
printName('Olga', apellido = 'Puchmajerova', reverso = Falso)
printName(apellido='Puchmajerova', nombre='Olga', reverso=Falso)

19Elnombre "abstracció n lambda" se deriva de algunas matemá ticas desarrolladas por


Alonzo Church en las dé cadas de 1930 y 1940.
Capter4.Divertidoacciones,Salbardilla,undABtr 3

Aunque los argumentos de palabras clave pueden aparecer en cualquier orden


en la lista de pará metros reales, no es legal seguir un argumento de palabra
clave con un argumento que no sea una palabra clave. Por lo tanto, un mensaje
de error sería producido por
printName('Olga', apellido = 'Puchmajerova', Falso)

Los argumentos de palabras clave se usan comú nmente junto con los valores de
pará metros predeterminados. Podemos, por ejemplo, escribir
def printName(firstName, lastName, reverse = False):
if reverse:
imprimir apellido + ', ' + nombre
más:
imprimir nombre, apellido

Los valores predeterminados permiten a los programadores llamar a una


funció n con menos argumentos que el nú mero especificado. Por ejemplo,
printName('Olga', 'Puchmajerova')
printName('Olga', 'Puchmajerova', True)
printName('Olga', 'Puchmajerova', reverse = True)

imprimirá
Olga Puchmajerova
Puchmajerova, Olga
Puchmajerova, Olga

Las dos ú ltimas invocaciones de printName son semá nticamente equivalentes. El


ú ltimo tiene la ventaja de proporcionar algo de documentació n para el quizá s
misterioso pará metro True.

4.1.3 Alcance
Veamos otro pequeñ o ejemplo,
def f(x): #nombre x usado como parámetro
formal y = 1
x = x + y
imprime 'x =',
x devuelve x

x = 3
y = 2
z = f(x) #valor de x usado como parámetro real
print 'z =', z
imprime 'x =',
x imprime 'y
=', y

Cuando se ejecuta, este có digo imprime,


x = 4
z = 4
x = 3
y = 2

¿Que esta pasando aqui? En la llamada de f, el pará metro formal x está ligado
localmente al valor del pará metro real x. Es importante notar que aunque los
pará metros reales y formales tienen el mismo nombre, no son la misma variable.
Cada funció n define un nuevo espacio de nombres, también llamado á mbito. El
3 Capter4.Divertidoacciones,Salbardilla,undABtr

El pará metro formal x y la variable local y que se usan en f existen solo dentro del
alcance de la definició n de f. La instrucció n de asignació n x = x + y dentro del cuerpo
de la funció n vincula el nombre local x con el objeto 4. Las asignaciones en f no
tienen ningú n efecto sobre las vinculaciones de los nombres x e y que existen fuera
del alcance de f.

Aquí hay una manera de pensar sobre esto:

 En el nivel superior, es decir, el nivel del shell, una tabla de símbolos


realiza un seguimiento de todos los nombres definidos en ese nivel y
sus enlaces actuales.
 Cuando se llama a una funció n, se crea una nueva tabla de símbolos (a veces
llamada marco de pila). Esta tabla realiza un seguimiento de todos los
nombres definidos dentro de la funció n (incluidos los pará metros formales)
y sus enlaces actuales. Si se llama a una funció n desde el cuerpo de la
funció n, se crea otro marco de pila.
 Cuando la funció n se completa, su marco de pila desaparece.
En Python, siempre se puede determinar el alcance de un nombre mirando el
texto del programa. Esto se denomina alcance está tico o lé xico. La figura 4.3
contiene un ejemplo un poco má s elaborado.

Figura 4.3 Ámbitos anidados


definición f(x):
La historia de los marcos de pila asociados
definición g(): con el có digo en la Figura 4.3 se
muestra en la Figura 4.4. x = 'abc'
imprime 'x
=', x
definición
h():
z = x
imprime 'z
=', zx = x + 1
imprime 'x
=', xh()
gramo()
imprime 'x
=', x
devuelve g

x = 3
z = f(x)
imprime 'x
Capter4.Divertidoacciones,Salbardilla,undABtr 3

Figura 4.4 Marcos de pila

La primera columna contiene el conjunto de nombres conocidos fuera del


cuerpo de la funció n f, es decir, las variables x y z, y el nombre de la funció n f. La
primera instrucció n de asignació n vincula x con 3.

La instrucció n de asignació n z = f(x) primero evalú a la expresió n f(x) invocando la


funció n f con el valor al que está vinculado x. Cuando se ingresa f, se crea un marco
de pila, como se muestra en la columna 2. Los nombres en el marco de pila son x (el
pará metro formal, no la x en el contexto de llamada), g y h. Las variables g y h está n
vinculadas a objetos de tipo funció n. Las propiedades de cada una de estas funciones
está n dadas por las definiciones de funció n dentro de f.

Cuando se invoca h desde dentro de f, se crea otro marco de pila, como se muestra en
la columna 3. Este marco contiene solo la variable local z. ¿Por qué no contiene
tambié n x? Se agrega un nombre al á mbito asociado con una funció n solo si ese
nombre es un pará metro formal de la funció n o una variable que está vinculada a un
objeto dentro del cuerpo de la funció n. En el cuerpo de h, x aparece solo en el lado
derecho de una instrucció n de asignació n. La aparició n de un nombre (x en este caso)
que no está ligado a ninguna parte del cuerpo de la funció n (el cuerpo de h) hace que
el intérprete busque el marco de pila anterior asociado con el á mbito dentro del cual
se define la funció n (el marco de pila asociado con f). Si se encuentra el nombre (que
es en este caso) se utiliza el valor al que está vinculado (4). Si no se encuentra allí, se
produce un mensaje de error.

Cuando h regresa, el marco de la pila asociado con la invocació n de h desaparece (es


decir, se saca de la parte superior de la pila), como se muestra en la columna 4.
Tenga en cuenta que nunca eliminamos marcos del medio de la pila, sino solo el
marco agregado má s recientemente. Debido a que tiene este comportamiento de
"ú ltimo en entrar, primero en salir", nos referimos a é l como una pila (piense en una
pila de bandejas que esperan ser tomadas en una cafetería).

A continuació n, se invoca g y se agrega un marco de pila que contiene la variable


local x de g (columna 5). Cuando g regresa, ese cuadro se abre (columna 6). Cuando
f regresa, el marco de pila que contiene los nombres asociados con f se extrae,
llevá ndonos de vuelta al marco de pila original (columna 7).

Tenga en cuenta que cuando f regresa, aunque la variable g ya no existe, el objeto


de tipo funció n al que alguna vez estuvo vinculado ese nombre todavía existe. Esto
es
4 Capter4.Divertidoacciones,Salbardilla,undABtr

porque las funciones son objetos y se pueden devolver como cualquier otro tipo
de objeto. Por lo tanto, z se puede vincular al valor devuelto por f, y la llamada de
funció n z() se puede usar para invocar la funció n que estaba vinculada al nombre
g dentro de f, aunque el nombre g no se conoce fuera del contexto de f. .

Entonces, ¿qué imprime el có digo de la Figura 4.3? se imprime


x = 4
z = 4
x = abc
x = 4
x = 3
z = <función g en 0x15b43b0>
x = abc

El orden en que ocurren las referencias a un nombre no es pertinente. Si un


objeto está vinculado a un nombre en cualquier parte del cuerpo de la funció n
(incluso si aparece en una expresió n antes de que aparezca como el lado
izquierdo de una asignació n), se trata como local para esa funció n.20

Consideremos, por ejemplo, el có digo


def f():
imprimir x

definición g():
imprimi
r xx =
1

x =
3f()
x =
3g()

Imprime 3 cuando se invoca f, pero se imprime un mensaje de error cuando


encuentra la declaració n de impresió n en g porque la declaració n de asignació n que
sigue a la declaració n de impresió n hace que x sea local para g. Y debido a que x es
local a g, no tiene valor cuando se ejecuta la declaració n de impresió n.

¿Confundido todavía? A la mayoría de la gente le toma un poco de tiempo


comprender las reglas de alcance. No dejes que esto te moleste. Por ahora, adelante
y comience a usar las funciones. La mayoría de las veces encontrará que solo quiere
usar variables que son locales a una funció n, y las sutilezas del alcance será n
irrelevantes.

20La sabiduría de esta decisió n de diseñ o de lenguaje es discutible.


Capter4.Divertidoacciones,Salbardilla,undABtr 4

4.2 Especificaciones
La figura 4.5 define una funció n, findRoot, que generaliza la bú squeda de bisecció n
que usamos para encontrar raíces cuadradas en la figura 4.1. Tambié n contiene una
funció n, testFindRoot, que se puede usar para probar si findRoot funciona o no
segú n lo previsto.

La funció n testFindRoot es casi tan larga como findRoot. Para los programadores sin
experiencia, escribir funciones de prueba como esta a menudo parece ser una pé rdida
de esfuerzo. Sin embargo, los programadores experimentados saben que una inversió n
en escribir có digo de prueba a menudo paga grandes dividendos. Ciertamente es mejor
que escribir casos de prueba en el shell una y otra vez durante la depuració n (el proceso
de descubrir por qué un programa no funciona y luego arreglarlo). También nos obliga a
pensar en qué pruebas son má s esclarecedoras.

El texto entre las comillas triples se llama docstring en Python. Por convenció n, los
programadores de Python usan docstrings para proporcionar especificaciones de
funciones. Se puede acceder a estas cadenas de documentos utilizando la funció n de
ayuda integrada.
Si ingresamos al shell y escribimos ayuda (abs), el sistema mostrará
Ayuda sobre la función incorporada abs en el
módulo incorporado: abs (...)
abs(número) -> número
Devuelve el valor absoluto del argumento.

Si el có digo de la Figura 4.5 (a continuació n) se cargó en IDLE, escriba


ayuda (buscar raíz)en el caparazó n se mostrará

Ayuda sobre la función findRoot en el módulo principal:

encontrarRaíz(x, potencia, épsilon)


Supone x y epsilon int o float, power an int, epsilon >
0 & power >= 1
Devuelve float y tal que y**power está dentro de épsilon de x.
Si tal flotador no existe, devuelve Ninguno

si escribimos
buscarraiz(

ya sea en el shell o en el editor, se mostrará la lista de pará metros formales y la


primera línea de la cadena de documentació n.
4 Capter4.Divertidoacciones,Salbardilla,undABtr

def buscarRaíz(x, potencia, épsilon):


"""Asume x y epsilon int o float, power an int, epsilon > 0 & power >= 1
Devuelve float y tal que y**power está dentro de épsilon de x.
Si tal flotante no existe, devuelve Ninguno""" si x < 0 y potencia%2 == 0:
volver Ninguno bajo = min(-1.0, x) alto = max(1.0, x)
respuesta = (alto + bajo)/2.0
while abs(respuesta**potencia - x) >= epsilon: si respuesta**potencia < x:
bajo = y más:
alto = respuesta
ans = (alto + bajo)/2.0 retorno ans

def testFindRoot(): épsilon = 0.0001


para x en (0.25, -0.25, 2, -2, 8, -8):
para potencia en rango (1, 4):
imprime 'Probando x = ' + str(x) +\
' and power = ' + str(power) result = findRoot(x, power, epsilon) if result == Ninguno:

imprimir 'más: Sin raíz'


imprimir '
', resultado**potencia, '~=', x

Figura 4.5 Hallar una aproximación a una raíz

Una especificació n de una funció n define un contrato entre el implementador de


una funció n y aquellos que escribirá n programas que usan la funció n. Nos
referiremos a los usuarios de una funció n como sus clientes. Se puede pensar que
este contrato contiene dos partes:

1. suposiciones: Describen las condiciones que deben cumplir los clientes


de la funció n. Por lo general, describen restricciones en los pará metros
reales. Casi siempre, especifican el conjunto aceptable de tipos para cada
pará metro y, no pocas veces, algunas restricciones sobre el valor de uno
o má s de los pará metros. Por ejemplo, las dos primeras líneas de la
cadena de documentació n debuscarraízdescribir los supuestos que deben
ser satisfechos por los clientes debuscarraíz.
2. Garantías: Estos describen las condiciones que debe cumplir la funció n,
siempre que se haya llamado de una manera que satisfaga los supuestos. Las
dos ú ltimas líneas de la cadena de documentació n debuscarraízdescribir las
garantías que debe cumplir la ejecució n de la funció n.

Las funciones son una forma de crear elementos computacionales que podemos
considerar primitivos. Así como tenemos las funciones integradas max y abs, nos
gustaría tener el equivalente de una funció n integrada para encontrar raíces y para
muchas otras operaciones complejas. Las funciones facilitan esto proporcionando
descomposició n y abstracció n.
Capter4.Divertidoacciones,Salbardilla,undABtr 4

Descomposicióncrea estructura. Nos permite dividir un problema en mó dulos


que son razonablemente autó nomos y que pueden reutilizarse en diferentes
entornos.

Abstracciónoculta detalle. Nos permite utilizar un fragmento de có digo como si


fuera una caja negra, es decir, algo cuyos detalles interiores no podemos ver, no
necesitamos ver y ni siquiera deberíamos querer ver.21 La esencia de la
abstracció n es preservar la informació n que es relevante en un contexto dado y
olvidar la informació n que es irrelevante en ese contexto. La clave para usar la
abstracció n de manera efectiva en la programació n es encontrar una noció n de
relevancia que sea apropiada tanto para el constructor de una abstracció n como
para los clientes potenciales de la abstracció n. Ese es el verdadero arte de
programar.

La abstracció n tiene que ver con el olvido. Hay muchas formas de modelar esto, por
ejemplo, el aparato auditivo de la mayoría de los adolescentes.

Adolescente dice: ¿Me prestas el auto esta noche?

El padre dice: Sí, pero regresa antes de la medianoche y asegú rate de que el
tanque de gasolina esté lleno.

Adolescente escucha: Sí.

El adolescente ha ignorado todos esos detalles molestos que considera


irrelevantes. La abstracció n es un proceso de muchos a uno. Si el padre hubiera
dicho Sí, pero que regresara antes de las 2:00 a. m. y se asegurara de que el
automó vil esté limpio, también se habría abstraído a Sí.

A modo de analogía, imagina que te piden que produzcas un curso de introducció n


a la informá tica que contenga veinticinco conferencias. Una forma de hacerlo sería
reclutar a veinticinco profesores y pedirles a cada uno de ellos que prepare una
conferencia de cincuenta minutos sobre su tema favorito. Aunque puede obtener
veinticinco horas maravillosas, es probable que todo se sienta como una
dramatizació n de "Seis personajes en busca de un autor" de Pirandello (o ese
curso de ciencias políticas que tomó con quince profesores invitados). Si cada
profesor trabajara de forma aislada, no tendría idea de có mo relacionar el material
de su clase con el material cubierto en otras clases.

De alguna manera, uno necesita que todos sepan lo que está n haciendo los demá s,
sin generar tanto trabajo que nadie esté dispuesto a participar. Aquí es donde entra
en juego la abstracció n. Se pueden escribir veinticinco especificaciones, cada una de
las cuales indica qué material deben aprender los estudiantes en cada clase, pero sin
dar ningú n detalle sobre có mo se debe enseñ ar ese material. Lo que obtuviste puede
no ser pedagó gicamente maravilloso, pero al menos podría tener sentido.

Esta es la forma en que las organizaciones usan equipos de programadores para


hacer las cosas. Dada una especificació n de un mó dulo, un programador puede
trabajar en la implementació n de ese mó dulo sin preocuparse demasiado por lo que
está n haciendo los otros programadores del equipo. Ademá s, los otros
programadores pueden usar la especificació n para comenzar a escribir có digo que
use ese mó dulo sin preocuparse demasiado por có mo se implementará ese mó dulo.

21“Donde la ignorancia es felicidad, es una locura ser sabio.”—Thomas Gray


4 Capter4.Divertidoacciones,Salbardilla,undABtr

La especificació n de findRoot es una abstracció n de todas las posibles


implementaciones que cumplen la especificació n. Los clientes de findRoot
pueden asumir que la implementació n cumple con la especificació n, pero no
deben asumir nada má s. Por ejemplo, los clientes pueden suponer que la llamada
encontrarRaíz(4.0, 2, 0.01)devuelve algú n valor cuyo cuadrado está entre3.99y
4.01. El valor devuelto puede ser positivo o negativo, y aunque
4,0 es un cuadrado perfecto, el valor devuelto puede no ser 2,0 o
-2,0.

4.3 recursividad
Es posible que haya oído hablar de la recursividad y, con toda probabilidad,
piense en ella como una técnica de programació n bastante sutil. Esa es una
leyenda urbana difundida por informá ticos para hacer creer a la gente que
somos má s inteligentes de lo que realmente somos.
La recursividad es una idea muy importante, pero no es tan sutil y es má s que una té cnica de
programació n.

Como método descriptivo, la recursividad es ampliamente utilizada, incluso por


personas que nunca soñ arían con escribir un programa.

Considere parte del có digo legal de los Estados Unidos que define la noció n de
ciudadano "natural". A grandes rasgos, la definició n es la siguiente

1. Cualquier niñ o nacido dentro de los Estados Unidos,


2. Cualquier niñ o nacido dentro del matrimonio fuera de los Estados
Unidos cuyos padres sean ciudadanos de los Estados Unidos, siempre y
cuando uno de los padres haya vivido en los Estados Unidos.
UU. antes del nacimiento del niñ o, y
3. Cualquier niñ o nacido dentro del matrimonio fuera de los Estados
Unidos, uno de cuyos padres es un ciudadano estadounidense que ha
vivido al menos cinco añ os en los EE. UU. antes del nacimiento del
niñ o, siempre que al menos dos de esos añ os hayan sido posteriores al
decimocuarto cumpleañ os del ciudadano.

La primera parte es simple; si nació en los Estados Unidos, es un ciudadano


natural (como Barack Obama). Si no nació en los EE. UU., entonces tiene que
decidir si sus padres son ciudadanos de los EE. UU. (ya sea natural o
naturalizados). Para determinar si sus padres son ciudadanos
estadounidenses, es posible que deba mirar a sus abuelos, etc.

En general, una definició n recursiva se compone de dos partes. Hay al menos un


caso base que especifica directamente el resultado para un caso especial (caso 1
en el ejemplo anterior), y hay al menos un caso recursivo (inductivo) (casos 2 y 3
en el ejemplo anterior) que define la respuesta en té rminos de la respuesta a la
pregunta sobre alguna otra entrada, típicamente una versió n má s simple del
mismo problema.
Capter4.Divertidoacciones,Salbardilla,undABtr 4

La definició n recursiva má s simple del mundo es probablemente la funció n


factorial (típicamente escrita en matemá ticas usando !) sobre nú meros
naturales.22 La definició n inductiva clá sica es,
1! = 1
(n + 1)! = (n + 1) * n!

La primera ecuació n define el caso base. La segunda ecuació n define factorial para
todos los nú meros naturales, excepto el caso base, en términos del factorial del
nú mero anterior.

La figura 4.6 contiene una implementació n iterativa (factI) y recursiva (factR) de


factorial.

Figura 4.6 Implementaciones iterativas y recursivas de factorial


def factI(n):
Esta funció n es lo"""Asume que n es
suficientemente un int
simple > para
como 0 que ninguna implementació n
Devuelve n!"""
sea difícil de seguir. Aú n así, la segunda es una traducció n má s obvia de la definició n
resultado = 1
recursiva original.mientras que
n > 1:
Casi parece una trampa implementar
resultado = factR llamando a factR desde dentro del cuerpo de
factR. Funciona por laresultado
misma razó*n nnque-= funciona la implementació n iterativa. Sabemos
1
que la iteració n, de hecho, terminará porque n comienza siendo positivo y cada vez que
resultado devuelto
se da la vuelta al bucle se reduce en 1. Esto significa que no puede ser mayor que 1 para
siempre. De manera similar, si factR se llama con 1, devuelve un valor sin realizar una
def factR(n):
llamada recursiva. Cuando hace
"""Asume que una
n esllamada
un intrecursiva,
> 0 siempre lo hace con un valor
Devuelve n!"""
uno menos que el valor con el que fue llamada. Eventualmente, la recursividad termina
si n == 1:
con la llamada factR(1).
volver

4.3.1 Números de Fibonacci


La secuencia de Fibonacci es otra funció n matemá tica comú n que generalmente
se define recursivamente. "Se reproducen como conejos" se usa a menudo para
describir una població n que el hablante cree que está creciendo demasiado
rá pido. En el añ o 1202, el

22La definició n exacta de "nú mero natural" está sujeta a debate. Algunos lo definen como los
nú meros enteros positivos y otros como los nú meros enteros no negativos. Es por eso que
fuimos explícitos acerca de los posibles valores de n en la cadena de documentació n de la
figura 4.6.
4 Capter4.Divertidoacciones,Salbardilla,undABtr

El matemá tico italiano Leonardo de Pisa, tambié n conocido como Fibonacci,


desarrolló una fó rmula diseñ ada para cuantificar esta noció n, aunque con
algunos supuestos no muy realistas.

Supongamos que un par de conejos recién nacidos, un macho y una hembra, se


colocan en un corral (o, peor aú n, se liberan en la naturaleza). Supongamos
ademá s que los conejos pueden aparearse a la edad de un mes (que,
sorprendentemente, algunas razas pueden hacerlo) y tienen un período de
gestació n de un mes (que, sorprendentemente, algunas razas tienen).
Finalmente, supongamos que estos conejos míticos nunca mueren, y que la
hembra siempre produce una nueva pareja (un macho, una hembra) cada mes a
partir del segundo mes. ¿Cuá ntas conejas preñ adas habrá al cabo de seis meses?

El ú ltimo día del primer mes (llá melo mes 0), habrá una hembra (lista para concebir
el primer día del pró ximo mes). El ú ltimo día del segundo mes, seguirá habiendo
una sola hembra (ya que no dará a luz hasta el primer día del mes siguiente). El
ú ltimo día del pró ximo mes habrá dos hembras (una preñ ada y otra no). El ú ltimo
día del pró ximo mes habrá tres hembras (dos preñ adas y una no). Etcétera. Veamos
esta progresió n en forma tabular.

Observe que en esta tabla, para el mes n > 1,


Mes Hembra
mujeres (n) = mujeres (n-1) + mujeres (n-2) . Esto no es un accidente. s
Cada hembra que estaba viva en el mes n-1seguirá vivo en un 0 1
mesnorte. Ademá s, cada hembra que estaba viva en el mesn- 1 1
2producirá una nueva hembra en un mesnorte. El nuevo
2 2
las hembras se pueden sumar a las hembras vivas en el mes n-1
para obtener el nú mero de hembras en el mes n. 3 3
4 5
El crecimiento de la població n se describe naturalmente por la
reaparición: 5 8
6 13
mujeres (0) = 1
mujeres (1) = 1
mujeres (n + 2) = mujeres (n + 1) + mujeres (n)

Esta definició n es un poco diferente de la definició n recursiva de factorial:

 Tiene dos casos base, no solo uno. En general, puede tener tantos
casos base como necesite.
 En el caso recursivo, hay dos llamadas recursivas, no solo una. Una vez
má s, puede haber tantos como necesite.

La figura 4.7 contiene una implementació n sencilla de la recurrencia de


Fibonacci,23 junto con una funció n que se puede usar para probarla.

23Aunque obviamente es correcto, esta es una implementació n terriblemente ineficiente de la


funció n de Fibonacci. Hay una implementació n iterativa simple que es mucho mejor.
Capter4.Divertidoacciones,Salbardilla,undABtr 4

def fib(n):
"""Asume n an int >= 0
Devuelve Fibonacci de
n"""
si n == 0 o n ==
1: devuelve 1
demás:
devolver fib(n-1) + fib(n-2)

def testFib(n):
para i en el rango (n+1):

Figura 4.7 Implementación recursiva de la secuencia de


Fibonacci

Escribir el có digo es la parte fá cil de resolver este problema. Una vez que
pasamos de la declaració n vaga de un problema sobre conejitos a un conjunto de
ecuaciones recursivas, el có digo casi se escribe solo. Encontrar algú n tipo de
forma abstracta de expresar una solució n al problema en cuestió n es muy a
menudo el paso má s difícil en la construcció n de un programa ú til. Hablaremos
mucho má s sobre esto má s adelante en el libro.

Como puede suponer, este no es un modelo perfecto para el crecimiento de las


poblaciones de conejos en la naturaleza. En 1859, Thomas Austin, un granjero
australiano, importó veinticuatro conejos de Inglaterra para usarlos como objetivos
en las cacerías. Diez añ os má s tarde, aproximadamente dos millones de conejos
fueron asesinados a tiros o atrapados cada añ o en Australia, sin un impacto notable
en la població n. Esos son muchos conejos, pero no se acercan al nú mero 120 de
Fibonacci.24

Aunque la secuencia de Fibonacci25 en realidad no proporciona un modelo


perfecto del crecimiento de las poblaciones de conejos, tiene muchas
propiedades matemá ticas interesantes. Los nú meros de Fibonacci tambié n son
bastante comunes en la naturaleza.26

Ejercicio de dedos:Cuando la implementació n dementiraen la figura 4.7 se utiliza


para calcularmentira(5), cuantas veces calcula el valormentira(2)?

24Eldañ o causado por los descendientes de esos veinticuatro lindos conejitos se ha estimado
en $ 600 millones por añ o, y está n en proceso de comerse muchas plantas nativas hasta la
extinció n.
25Que llamemos a esto una secuencia de Fibonacci es un ejemplo de una interpretació n
eurocé ntrica de la historia. La gran contribució n de Fibonacci a las matemá ticas europeas
fue su libro Liber Abaci, que introdujo a los matemá ticos europeos muchos conceptos ya
bien conocidos por los eruditos indios y á rabes. Estos conceptos incluían nú meros
ará bigos hindú es y el sistema decimal. Lo que hoy llamamos la secuencia de Fibonacci se
tomó del trabajo del matemá tico sá nscrito Pingala.
26Si te sientes especialmente geek, trata de escribir un poema de Fibonacci. Esta es una
forma de poesía en la que el nú mero de sílabas en cada línea es igual al nú mero total de
sílabas en las dos líneas anteriores. Piensa en la primera línea (que tiene cero sílabas) como
un lugar para respirar profundamente antes de comenzar a leer tu poema.
4 Capter4.Divertidoacciones,Salbardilla,undABtr

4.3.2 palíndromos
La recursividad tambié n es ú til para muchos problemas que no involucran nú meros. Cifra
4.8 contiene una funció n, isPalindrome, que verifica si una cadena se lee de la
misma manera hacia adelante y hacia atrá s.

Figura 4.8 Prueba de palíndromo


def isPalindrome(s):
"""Asume
La funció que s escontiene dos funciones auxiliares internas. Esto no debería
n isPalindrome
una cadena
ser de interé s para los
Devuelve clientes
True de letras
si las la funcióen
n, asquienes
forman solo les debe importar que
un palíndromo;
isPalindrome cumpla
Falso en con
casosus especificaciones.
contrario. Pero debería
Se ignoran importarle,y porque hay
las mayúsculas
minúsculas."""
cosas que aprender al examinar la implementació n.
defn auxiliar toChars convierte todas las letras a minú sculas y elimina todas las
La funció
toChars(s):
que no sons letras.
=
Comienza usando un método integrado en cadenas para generar una
cadena que es idéntica a s, excepto que todas las letras mayú sculas se han convertido a
s.lower()
letras
minú sculas. = '' mucho má s sobre la invocació n de métodos cuando lleguemos
Hablaremos
para c en
a las clases. Por ahora, considé relo como una sintaxis peculiar para una llamada de
s:
funció n. En lugar
si c deenponer el primer argumento (y en este caso solo) entre paréntesis
después del nombre de la funció n, usamos la notació n de puntos para colocar ese
'abcdefghijklmnopqrstuvwxyz':
letras
argumento antes del nombre = letras + c n.
de la funció
devolver cartas
La funció n auxiliar isPal usa recursividad para hacer el trabajo real. Los dos casos
defcadenas
base son es amigo(s):
de longitud cero o uno. Esto significa que la parte recursiva de la
implementació n se alcanza solo en cadenas de longitud dos o má s. La conjunció n 27
en la clá usula else se evalú a de izquierda a derecha. El có digo primero comprueba si
el primer y el ú ltimo cará cter son iguales y, si lo son, comprueba si la cadena menos
esos dos caracteres es un palíndromo.
Que el segundo conjunto no se evalú a a menos que el primer conjunto se evalú e como

27Cuando dos expresiones con valores booleanos está n conectadas por "y", cada
expresió n se llama conjunto. Si está n conectados por “o”, se llaman disjuntos.
Capter4.Divertidoacciones,Salbardilla,undABtr 4

True es semánticamente irrelevante en este ejemplo. Sin embargo,


más adelante en el libro veremos ejemplos en los que este tipo de
evaluación de cortocircuito de expresiones booleanas es
semánticamente relevante.

Esta implementació n de isPalindrome es un ejemplo de un principio de resolució n de


problemas conocido como divide y vencerá s. (Este principio está relacionado pero es
diferente de los algoritmos de divide y vencerá s, que se analizan en el Capítulo 10.) El
principio de resolució n de problemas es conquistar un problema difícil dividiéndolo
en un conjunto de subproblemas con las propiedades que

 los subproblemas son má s fá ciles de resolver que el problema original, y


 las soluciones de los subproblemas se pueden combinar para resolver
el problema original.
En este caso, resolvemos el problema dividiendo el problema original en una
versió n má s simple del mismo problema (comprobando si una cadena má s corta
es un palíndromo), ademá s de algunas cosas simples que sabemos hacer
(comparando caracteres individuales). La figura 4.9 contiene có digo que se puede
usar para visualizar có mo funciona esto.

def Figura 4.9 Código para visualizar la prueba del palíndromo


isPalindrome(s):
"""Asume que s es
una cadena
Devuelve True si s es un palíndromo; Falso en caso
contrario. Se ignoran los signos de puntuación,
los espacios en blanco y las mayúsculas."""

def
toChars(s):
s =
s.lower()
letras = ''
para c en
s:
si c en
'abcdefghijklmnopqrstuvwxyz':
letras = letras + c
devolver cartas

def es amigo(s):
print ' isPal llamó con', s
if len(s) <= 1:
print 'A punto de devolver True desde el
caso base' return True
demás:
respuesta = s[0] == s[-1] and isPal(s[1:-1])
print 'A punto de regresar', answer, 'for',
s return answer

return

isPal(toChars(s)) def
5 Capter4.Divertidoacciones,Salbardilla,undABtr

Cuando se ejecuta el có digo de la Figura 4.9, se imprimirá


Prueba perroDios
isPal llamado con doggod
isPal llamado con oggo
isPal llamado con gg
isPal llamado con
A punto de devolver True desde el
caso base A punto de devolver True
para gg
A punto de devolver True para
oggo A punto de devolver True
para doggod
Verdadero
Prueba doGood
isPal llamado con dogood
isPal llamado con ogoo
isPal llamado con go
A punto de regresar Falso para
ir A punto de regresar Falso
para ogoo A punto de regresar
Falso para hacer el bien
FALSO

Divide y vencerá s es una idea muy antigua. Julio Cé sar practicó lo que los
romanos llamaban divide et impera (divide y vencerá s). Los britá nicos lo
practicaron brillantemente para controlar el subcontinente indio. Benjamin
Franklin era muy consciente de la experiencia britá nica en el uso de esta té cnica,
lo que lo llevó a decir en la firma de la Declaració n de Independencia de los EE.
UU.: "Debemos estar todos juntos, o seguramente todos seremos colgados por
separado".

4.4 Variables globales


Si intentó llamar a fib con un nú mero grande, probablemente notó que tardó mucho
tiempo en ejecutarse. Supongamos que queremos saber cuá ntas llamadas recursivas
se realizan. Podríamos hacer un aná lisis cuidadoso del có digo y resolverlo, y en el
Capítulo 9 hablaremos sobre có mo hacerlo. Otro enfoque es agregar algú n có digo
que cuente el nú mero de llamadas. Una forma de hacerlo usa variables globales.

Hasta ahora, todas las funciones que hemos escrito se comunican con su entorno
ú nicamente a travé s de sus pará metros y valores de retorno. En su mayor parte,
esto es exactamente como debería ser. Por lo general, conduce a programas que
son relativamente fá ciles de leer, probar y depurar. De vez en cuando, sin
embargo, las variables globales resultan ú tiles. Considere el có digo de la figura
4.10.
Capter4.Divertidoacciones,Salbardilla,undABtr 5

def fib(x):
"""Asume x an int >= 0
Devuelve Fibonacci de
x"""
global
numFibCalls
numFibCalls += 1
si x == 0 o x ==
1: devuelve 1
demás:
devolver fib(x-1) + fib(x-2)

def testFib(n):
for i in range(n+1):
global
numFibCalls
numFibCalls = 0
Figura 4.10 Uso de una variable global

En cada funció n, la línea de có digo global numFibCalls le dice a Python que el nombre
numCalls debe definirse en el á mbito má s externo del mó dulo (consulte la Secció n 4.5)
en el que aparece la línea de có digo en lugar de dentro del á mbito de la funció n en la que
la línea de có digo aparece, a pesar del hecho de que numFibCalls aparece en el lado
izquierdo de una instrucció n de asignació n tanto en fib como en testFib. (Si no
hubié ramos incluido el có digo global numFibCalls, el nombre numFibCalls habría sido
local para fib y testFib). Las funciones fib y testFib tienen acceso ilimitado al objeto al
que hace referencia la variable numFibCalls. La funció n testFib vincula numFibCalls a 0
cada vez que llama fib, y fib incrementa el valor de numFibCalls cada vez que se ingresa
fib.

Es con cierta inquietud que presentamos el tema de las variables globales. Desde la
dé cada de 1970, los científicos de la computació n que llevan la tarjeta han
vituperado contra ellos. El uso indiscriminado de variables globales puede generar
muchos problemas. La clave para hacer que los programas sean legibles es la
localidad. Uno lee un programa pieza por pieza, y cuanto menos contexto se necesite
para entender cada pieza, mejor. Dado que las variables globales se pueden
modificar o leer en una amplia variedad de lugares, el uso descuidado de ellas puede
destruir la localidad. Sin embargo, hay momentos en que son justo lo que se
necesita.

4.5 Módulos
Hasta ahora, hemos operado bajo la suposició n de que todo nuestro programa está
almacenado en un archivo. Esto es perfectamente razonable siempre que los
programas sean pequeñ os. Sin embargo, a medida que los programas se hacen má s
grandes, normalmente es má s conveniente almacenar diferentes partes de ellos en
archivos diferentes. Imagine, por ejemplo, que varias personas está n trabajando en el
mismo programa. Sería una pesadilla si todos intentaran actualizar el mismo archivo.
Los mó dulos de Python nos permiten construir fá cilmente un programa a partir del
có digo en varios archivos.

Un mó dulo es un archivo .py que contiene definiciones y declaraciones de Python.


Podríamos crear, por ejemplo, un archivo circle.py que contenga
5 Capter4.Divertidoacciones,Salbardilla,undABtr

pi = 3.14159

área definida (radio):


devolver pi*(radio**2)

def circunferencia(radio):
devuelve 2*pi*radio

def
superficieesfera(radio)
: return
4.0*area(radio)

def volumen de la esfera(radio):


retorno (4.0/3.0)*pi*(radio**3)

Un programa obtiene acceso a un mó dulo a travé s de una declaració n de


importació n. Así, por ejemplo, el có digo
importar
círculo
imprimir
círculo.pi
imprimir circulo.area(3)
imprimir
círculo.circunferencia(3)
imprimir
círculo.superficieesfera(3)

imprimirá
3.14159
28.27431
18.84954
113.09724

Los mó dulos normalmente se almacenan en archivos individuales. Cada mó dulo


tiene su propia tabla de símbolos privada. En consecuencia, dentro de circle.py
accedemos a los objetos (p. ej., pi y á rea) de la forma habitual. La ejecució n de la
importació n M crea un enlace para el mó dulo M en el á mbito en el que se produce la
importació n. Por lo tanto, en el contexto de importació n usamos la notació n de
puntos para indicar que nos referimos a un nombre definido en el mó dulo
importado.28 Por ejemplo, fuera de circle.py, las referencias pi y circle.pi pueden (y
en este caso lo hacen) referirse a diferentes objetos.

A primera vista, el uso de la notació n de puntos puede parecer engorroso. Por otro
lado, cuando uno importa un mó dulo, a menudo no tiene idea de qué nombres
locales podrían haberse usado en la implementació n de ese mó dulo. El uso de la
notació n de puntos para calificar completamente los nombres evita la posibilidad de
quemarse por un choque accidental de nombres. Por ejemplo, la declaració n de
asignació n pi = 3.0 no cambia el valor de pi utilizado dentro del mó dulo de círculo.

Existe una variante de la declaració n de importació n que permite que el programa de


importació n omita el nombre del mó dulo al acceder a los nombres definidos dentro del
mó dulo importado. Ejecutar la declaració n de M import * crea enlaces en el á mbito
actual a todos los objetos definidos dentro de M, pero no a M mismo. Por ejemplo, el
có digo
desde círculo
importar * imprimir
pi
círculo de impresión.pi
Capter4.Divertidoacciones,Salbardilla,undABtr
28Superficialmente, esto puede parecer no relacionado con el uso de la notació n de puntos en la
5
invocació n de mé todos. Sin embargo, como veremos en el Capítulo 8, existe una conexió n
profunda.
5 Capter4.Divertidoacciones,Salbardilla,undABtr

primero imprimirá 3.14159, y luego producirá el mensaje de error


NameError: el nombre 'círculo' no está definido

Algunos programadores de Python desaprueban el uso de esta forma de


importació n porque creen que hace que el có digo sea má s difícil de leer.

Como hemos visto, un mó dulo puede contener sentencias ejecutables así como
definiciones de funciones. Por lo general, estas declaraciones se utilizan para
inicializar el mó dulo. Por esta razó n, las declaraciones en un mó dulo se ejecutan
solo la primera vez que se importa un mó dulo a un programa. En una nota
relacionada, un mó dulo se importa solo una vez por sesió n de inté rprete. Si
inicia IDLE, importa un mó dulo y luego cambia el contenido de ese mó dulo, el
inté rprete seguirá usando la versió n original del mó dulo. Esto puede conducir a
un comportamiento desconcertante durante la depuració n. Puede obligar al
inté rprete a recargar todos los mó dulos importados ejecutando reload().

Hay muchos mó dulos ú tiles que vienen como parte de la biblioteca está ndar de
Python. Por ejemplo, rara vez es necesario escribir sus propias
implementaciones de funciones matemá ticas o de cadenas comunes. Puede
encontrar una descripció n de esta biblioteca
enhttp://docs.python.org/2/library/.

4.6 archivos
Cada sistema informá tico utiliza archivos para guardar cosas de un cá lculo al
siguiente. Python proporciona muchas facilidades para crear y acceder a archivos.
Aquí ilustramos algunos de los bá sicos.

Cada sistema operativo (p. ej., Windows y MAC OS) viene con su propio sistema de
archivos para crear y acceder a archivos. Python logra la independencia del sistema
operativo al acceder a los archivos a travé s de algo llamado identificador de archivo.
El có digo
nameHandle = open('niños', 'w')

indica al sistema operativo que cree un archivo con el nombre kids y devuelva un
identificador de archivo para ese archivo. El argumento 'w' para abrir indica que el
archivo debe abrirse para escritura. El siguiente có digo abre un archivo, usa el
mé todo de escritura para escribir dos líneas y luego cierra el archivo. Es importante
recordar cerrar el archivo cuando el programa termine de usarlo. De lo contrario,
existe el riesgo de que algunas o todas las escrituras no se guarden.
nameHandle = open('kids', 'w')
for i in range(2):
nombre = raw_input('Ingrese el
nombre:')
nameHandle.write(nombre + '\n')
nombreManejador.close()

En una cadena, el cará cter "\" es un cará cter de escape que se utiliza para
indicar que el siguiente cará cter debe tratarse de manera especial. En este
ejemplo, la cadena '\n' indica un cará cter de nueva línea.
Capter4.Divertidoacciones,Salbardilla,undABtr 5

Ahora podemos abrir el archivo para lectura (usando el argumento 'r') e


imprimir su contenido. Dado que Python trata un archivo como una secuencia
de líneas, podemos usar una instrucció n for para iterar sobre el contenido del
archivo.
nameHandle = open('kids', 'r')
for line in nameHandle:
línea de
impresiónnombreManejador
.close()

Si hubiésemos tecleado los nombres de David y Andrea, esto se imprimiría


David

Andrea

La línea extra entre David y Andrea está ahí porque print comienza una nueva línea
cada vez que encuentra el '\n' al final de cada línea en el archivo. Podríamos haber
evitado imprimir eso escribiendo print line[:-1]. Ahora considera
identificador de nombre =
open('niños', 'w')
identificador de
nombre.write('Michael\n')
identificador de
nombre.write('Mark\n')
identificador de
nombre.close()
nameHandle = open('kids', 'r')
for line in nameHandle:
línea de
impresión[:-1]
nameHandle.close()

se imprimirá
miguel
marca

Observe que hemos sobrescrito el contenido anterior del archivo kids. Si no


queremos hacer eso, podemos abrir el archivo para agregarlo (en lugar de escribirlo)
usando el argumento 'a'.

Por ejemplo, si ahora ejecutamos el có digo

identificador de nombre =
open('niños', 'a')
identificador de
nombre.write('David\n')
identificador de
nombre.write('Andrea\n')
identificador de
nombre.close()
nameHandle = open('kids', 'r')
for line in nameHandle:
línea de
impresión[:-1]
nameHandle.close()

imprimirá
Michael
Marcos
David
Andrea
5 Capter4.Divertidoacciones,Salbardilla,undABtr

Algunas de las operaciones comunes en los archivos se resumen en la Figura 4.11.

abierto Figura 4.11 Funciones


(fn, 'w')fn comunes
es una cadena que para accederun
representa a archivos
nombre de
archivo. Crea un archivo para escribir y devuelve un
identificador de archivo.
abierto (fn, 'r')fn es una cadena que representa un nombre de
archivo. Abre un archivo existente para lectura y devuelve un
identificador de archivo.
abierto(fn, 'a')fn es una cadena que representa un nombre de
archivo. Abre un archivo existente para agregarlo y devuelve
un identificador de archivo.
fh.leer()devuelve una cadena que contiene el contenido del archivo
asociado con el identificador de archivo fh.
fh.readline()devuelve la siguiente línea en el archivo asociado con el
identificador de archivo fh.
fh.readlines()devuelve una lista en la que cada elemento es una línea del
archivo asociado con el identificador de archivo fh.
fh.escribir(es)escriba la cadena s al final del archivo asociado con el
identificador de archivo fh.
fh.writeLines(S)S es una secuencia de cadenas. Escribe cada
elemento de S en el archivo asociado con el identificador de
archivo fh.
Capter4.Divertidoacciones,Salbardilla,undABtr 5

5ESTRUCTURADO TIPOS, MUTABILIDAD Y MAYOR-


FUNCIONES DE ORDEN
Los programas que hemos visto hasta ahora se han ocupado de tres tipos de
objetos: int, float y str. Los tipos numéricos int y float son tipos escalares. Es decir,
objetos sin estructura interna accesible. Por el contrario, str puede considerarse
como un tipo estructurado o no escalar. Se puede usar la indexació n para extraer
caracteres individuales de una cadena y el corte para extraer subcadenas.

En este capítulo, presentamos tres tipos estructurados. Uno, tupla, es una


generalizació n bastante simple de str. Los otros dos, list y dict, son má s
interesantes, en parte porque son mutables. También volvemos al tema de las
funciones con algunos ejemplos que ilustran la utilidad de poder tratar funciones
de la misma forma que otro tipo de objetos.

5.1 tuplas
Al igual que las cadenas, las tuplas son secuencias ordenadas de elementos. La
diferencia es que los elementos de una tupla no necesitan ser caracteres. Los
elementos individuales pueden ser de cualquier tipo y no es necesario que sean del
mismo tipo entre sí.

Los literales de tipo tupla se escriben encerrando entre paré ntesis una lista de
elementos separados por comas. Por ejemplo, podemos escribir
t1 = ()
t2 = (1, 'dos', 3)
imprimir
t1
imprimir
t2

Como era de esperar, las declaraciones de impresió n producen la salida


()
(1, 'dos', 3)

Mirando este ejemplo, es posible que se le haga creer que la tupla que contiene el
valor ú nico 1 se escribiría (1). Pero, para citar a Richard Nixon, “eso estaría mal”.
Dado que los paré ntesis se utilizan para agrupar expresiones, (1) es simplemente
una forma detallada de escribir el nú mero entero 1. Para indicar la tupla singleton
que contiene este valor, escribimos (1,). Casi todos los que usan Python en un
momento u otro han omitido accidentalmente esa molesta coma.

Al igual que las cadenas, las tuplas se pueden concatenar, indexar y dividir. Considerar
t1 = (1, 'dos', 3)
t2 = (t1, 3,25)
imprimir t2
imprimir (t1 + t2)
imprimir (t1 + t2)
[3] imprimir (t1 +
t2)[2:5]

La segunda declaració n de asignació n vincula el nombre t2 a una tupla que


contiene la tupla a la que está vinculado t1 y el nú mero de punto flotante 3.25. Esto
es
Capítulo 5. Tipos estructurados, mutabilidad y funciones de orden 5

posible porque una tupla, como todo lo demá s en Python, es un objeto, por lo que
las tuplas pueden contener tuplas. Por lo tanto, la primera declaració n de impresió n
produce la salida,
((1, 'dos', 3), 3.25)

La segunda declaració n de impresió n imprime el valor generado al concatenar los


valores vinculados a t1 y t2, que es una tupla con cinco elementos. Produce la
salida
(1, 'dos', 3, (1, 'dos', 3), 3.25)

La siguiente declaració n selecciona e imprime el cuarto elemento de la tupla


concatenada (como siempre en Python, la indexació n comienza en 0), y la
siguiente declaració n crea e imprime una porció n de esa tupla, produciendo
la salida
(1, 'dos', 3)
(3, (1, 'dos', 3), 3.25)

Se puede usar una instrucció n for para iterar sobre los elementos de una tupla.
Por ejemplo, el siguiente có digo imprime los divisores comunes de 20 y 100 y
luego la suma de todos los divisores.
def buscarDivisores (n1, n2):
"""Asume que n1 y n2 son enteros positivos
Devuelve una tupla que contiene todos los divisores comunes de
n1 y n2""" divisores = () #la tupla vacía
para i en el rango (1, min (n1, n2)
+ 1): si n1%i == 0 y n2%i == 0:
divisores = divisores +
(i,) devuelve divisores

divisores = findDivisors(20,
100) imprimir divisores
totales = 0
para d en
divisores:
total += d
imprimir total

5.1.1 Secuencias y Asignación Múltiple


Si conoce la longitud de una secuencia (por ejemplo, una tupla o una cadena), puede
ser conveniente usar la declaració n de asignació n mú ltiple de Python para extraer
los elementos individuales. Por ejemplo, después de ejecutar la declaració n x, y = (3,
4), x se vinculará a 3 e y a 4. De manera similar, la declaració n a, b, c = 'xyz' vinculará
a a 'x', b a 'y', yc a 'z'.

Este mecanismo es particularmente conveniente cuando se usa junto con


funciones que devuelven secuencias de tamañ o fijo.
5 Capítulo 5. Tipos estructurados, mutabilidad y funciones de orden

Consideremos, por ejemplo, la funció n


def buscarDivisoresExtremos(n1, n2):
"""Asume que n1 y n2 son enteros positivos Devuelve
una tupla que contiene el mínimo común divisor >
1 y el máximo común divisor de n1 y n2"""
divisores = () #la tupla vacía
minVal, maxVal = Ninguno,
Ninguno
para i en rango(2, min(n1, n2) +
1): si n1%i == 0 y n2%i == 0:
si minVal == Ninguno o i <
minVal: minVal = i
si maxVal == Ninguno o i >
maxVal: maxVal = i
retorno (minVal, maxVal)

La declaració n de asignació n mú ltiple


minDivisor, maxDivisor = buscarExtremeDivisors(100, 200)

se unirá minDivisora2yMaxDivisora100.

5.2 Listas y Mutabilidad


Al igual que una tupla, una lista es una secuencia ordenada de valores, donde
cada valor se identifica mediante un índice. La sintaxis para expresar literales de
tipo lista es similar a la utilizada para tuplas; la diferencia es que usamos
corchetes en lugar de paré ntesis. La lista vacía se escribe como [], y las listas
singleton se escriben sin esa coma (tan fá cil de olvidar) antes del corchete de
cierre. Entonces, por ejemplo, el có digo,
L = ['Lo hice todo', 4, 'amor']
for i in range(len(L)):
imprimir L[i]

produce la salida,
lo hice todo
4
amar

En ocasiones, el hecho de que los corchetes se utilicen para literales de tipo lista,
indexació n en listas y segmentació n de listas puede generar cierta confusió n
visual. Por ejemplo, la expresió n [1,2,3,4][1:3][1], que da como resultado 3, usa
los corchetes de tres maneras diferentes. Esto rara vez es un problema en la
prá ctica, porque la mayoría de las veces las listas se crean de forma incremental
en lugar de escribirse como literales.

Las listas se diferencian de las tuplas en un aspecto muy importante: las listas son
mutables. Por el contrario, las tuplas y las cadenas son inmutables. Hay muchos
operadores que se pueden usar para crear objetos de estos tipos inmutables, y las
variables se pueden vincular a objetos de estos tipos. Pero los objetos de tipos
inmutables no se pueden modificar.
Por otro lado, los objetos de tipo lista se pueden modificar después de su creació n.

La distinció n entre mutar un objeto y asignar un objeto a una variable puede, al


principio, parecer sutil. Sin embargo, si sigues repitiendo el mantra, “En
Capítulo 5. Tipos estructurados, mutabilidad y funciones de orden 5

Python, una variable es simplemente un nombre, es decir, una etiqueta que se


puede adjuntar a un objeto”, le brindará claridad.

Cuando las declaraciones

Tecnologías = ['MIT', 'Caltech']


Ivys = ['Harvard', 'Yale', 'Brown']

se ejecutan, el inté rprete crea dos nuevas listas y les vincula las variables
apropiadas, como se muestra a continuació n.

Figura 5.1 Dos listas

Las sentencias de asignació n


Univs = [Técnicos, Ivys]
Universidad1 = [['MIT', 'Caltech'], ['Harvard', 'Yale', 'Brown']]

tambié n cree nuevas listas y vincule variables a ellas. Los elementos de estas listas son en sí
mismos listas. Las tres declaraciones impresas
print 'Univ =', Univ
print 'Univ1 =', Univ1
print Univ == Univ1

producir la salida
Universidad = [['MIT', 'Caltech'], ['Harvard', 'Yale', 'Brown']]
Univs1 = [['MIT', 'Caltech'], ['Harvard', 'Yale', 'Brown']]
Verdadero

Parece como si Univs y Univs1 estuvieran vinculados al mismo valor. Pero las
apariencias pueden engañ ar. Como ilustra la siguiente imagen, Univs y Univs1 está n
vinculados a valores bastante diferentes.
6 Capítulo 5. Tipos estructurados, mutabilidad y funciones de orden

Figura 5.2 Dos listas que parecen tener el mismo valor, pero no lo tienen

El hecho de que Univs y Univs1 estén vinculados a diferentes objetos se puede


verificar utilizando la funció n integrada de Python id, que devuelve un identificador
entero ú nico para un objeto. Esta funció n nos permite probar la igualdad de objetos.
Cuando ejecutamos el có digo
print Univs == Univs1 #test valor igualdad
print id(Univs) == id(Univs1) #test objeto igualdad
print 'Id of Univs =', id(Univs)
imprimir 'Id de Univs1 =', id(Univs1)

se imprime
Verda
dero
Falso
Id de Univs = 24499264
Id de Univs1 = 24500504

(No espere ver los mismos identificadores ú nicos si ejecuta este có digo. La
semá ntica de Python no dice nada sobre qué identificador está asociado con cada
objeto; simplemente requiere que no haya dos objetos que tengan el mismo
identificador).

Observe que en la figura 5.2 los elementos de Univs no son copias de las listas a las
que está n vinculados Techs e Ivys, sino que son las listas mismas. Los elementos de
Univs1 son listas que contienen los mismos elementos que las listas de Univs, pero
no son las mismas listas. Podemos ver esto ejecutando el có digo

print 'Ids de Univs[0] y Univs[1]', id(Univs[0]), id(Univs[1]) print


'Ids de Univs1[0] y Univs1[1]', id(Univs1[0 ]), id(Univ1[1])

que imprime
Id. de Univs[0] y Univs[1] 22287944 22286464 Id.
de Univs1[0] y Univs1[1] 22184184 22287984

¿Por qué importa esto? Importa porque las listas son mutables.

Considere el có digo
Techs.append('RPI')
Capítulo 5. Tipos estructurados, mutabilidad y funciones de orden 6

El mé todo append tiene un efecto secundario. En lugar de crear una nueva lista,
muta la lista Techs existente agregando un nuevo elemento, la cadena 'RPI', al final
de la misma.

Despué s de ejecutar append, el estado del cá lculo se ve así

Figura 5.3 Demostración de mutabilidad

Univs todavía contiene las mismas dos listas, pero el contenido de


una de esas listas ha cambiado. En consecuencia, las sentencias
impresas

imprimir 'Univs =',


Univers imprimir 'Univs1
=', Univers1

ahora producir la salida


Universidades = [['MIT', 'Caltech', 'RPI'], ['Harvard', 'Yale', 'Brown']]
Universidad1 = [['MIT', 'Caltech'], ['Harvard', 'Yale', 'Brown']]

Lo que tenemos aquí es algo llamado aliasing. Hay dos caminos distintos al
mismo objeto de lista. Una ruta es a travé s de la variable Techs y la otra a travé s
del primer elemento del objeto de lista al que está vinculado Univs. Uno puede
mutar el objeto a travé s de cualquier camino, y el efecto de la mutació n será
visible a travé s de ambos caminos. Esto puede ser conveniente, pero tambié n
puede ser traicionero.
La creació n de alias no intencional conduce a errores de programació n que a
menudo son enormemente difíciles de rastrear.

Al igual que con las tuplas, se puede usar una instrucció n for para iterar sobre los
elementos de una lista. Por ejemplo,
para e en Univs:
imprime 'Univs contiene',
e imprime 'que contiene'
para u en e:
imprimir'', tu
6 Capítulo 5. Tipos estructurados, mutabilidad y funciones de orden

imprimirá
Univs contiene ['MIT', 'Caltech', 'RPI']
que contiene
MIT
RPI de
Caltech
Univs contiene ['Harvard', 'Yale', 'Brown']
que contiene
Harvard
Yale
Marrón

Cuando agregamos una lista a otra, por ejemplo, Techs.append(Ivys), se


mantiene la estructura original. Es decir, el resultado es una lista que contiene
una lista. Supongamos que no queremos mantener esta estructura, pero
queremos agregar los elementos de una lista a otra lista. Podemos hacerlo
usando la concatenació n de listas o el mé todo extender, por ejemplo,
L1 = [1,2,3]
L2 = [4,5,6]
L3 = L1 + L2
imprimir 'L3 =',
L3
L1.extender(L2)
imprimir 'L1 =',
L1
L1.agregar(L2)
imprimir 'L1 =',
L1

imprimirá
L3 = [1, 2, 3, 4, 5, 6]
L1 = [1, 2, 3, 4, 5, 6]
L1 = [1, 2, 3, 4, 5, 6, [4, 5, 6]]

Tenga en cuenta que el operador + no tiene un efecto secundario. Crea una nueva
lista y la devuelve. Por el contrario, extienda y agregue cada L1 mutado.

La figura 5.4 contiene breves descripciones de algunos de los mé todos asociados


con las listas. Tenga en cuenta que todos estos, excepto el recuento y el índice,
mutan la lista.

L.añadir(e)agregaFigura
el objeto
5.4 al final deLasociados
miMétodos . a listas
L.cuenta(e)devuelve el nú mero de veces que aparece e enL.
L.insertar(i, e)inserta el objetomienLen el
índicei.L.extender(L1)agrega los elementos en la listaL1al
final deL.L.quitar(e)elimina la primera aparició n demideL.
índice L(e)devuelve el índice de la primera aparició n de e en L. Genera una
excepció n (consulte el Capítulo 7) si e no está en L.
L pop (i)elimina y devuelve el elemento en el índice i en L. Si se omite i, el valor
predeterminado es -1, para eliminar y devolver el ú ltimo elemento de L.
Ordenar L()ordena los elementos deL en orden ascendente.
L.reversa()invierte el orden de los elementos en L.
Capítulo 5. Tipos estructurados, mutabilidad y funciones de orden 6

5.2.1 Clonación
Aunque está permitido, por lo general es prudente evitar mutar una lista sobre la
que se está iterando. Consideremos, por ejemplo, el có digo
def removeDups(L1, L2):
"""Supone que L1 y L2 son listas.
Elimina cualquier elemento de L1 que también ocurra en
L2""" para e1 en L1:
si e1 en L2:
L1.eliminar(e1
) L1 = [1,2,3,4]
L2 = [1,2,5,6]
removeDups(L1, L2)
imprime 'L1 =', L1

Es posible que se sorprenda al descubrir que la declaració n de impresió n


produce la salida
L1 = [2, 3, 4]

Durante un bucle for, la implementació n de Python realiza un seguimiento de dó nde


se encuentra en la lista mediante un contador interno que se incrementa al final de
cada iteració n.
Cuando el valor del contador alcanza la longitud actual de la lista, el ciclo
termina. Esto funciona como cabría esperar si la lista no está mutada dentro del
ciclo, pero puede tener consecuencias sorprendentes si la lista está mutada. En
este caso, el contador oculto comienza en 0, descubre que L1[0] está en L2 y lo
elimina, reduciendo la longitud de L1 a 3. Luego, el contador se incrementa a 1 y
el có digo procede a verificar si el el valor de L1[1] está en L2. Observe que este
no es el valor original de L1[1] (es decir, 2), sino el valor actual de L1[1] (es
decir, 3). Como puede ver, es posible averiguar qué sucede cuando se modifica la
lista dentro del bucle. Sin embargo, no es fá cil. Y es probable que lo que suceda
no sea intencional, como en este ejemplo.

Una forma de evitar este tipo de problema es usar el corte para clonar (es decir,
hacer una copia) de la lista y escribir para e1 en L1[:]. Observe que escribir newL1 =
L1 seguido de for e1 en newL1 no habría resuelto el problema. No habría creado una
copia de L1, sino que simplemente habría introducido un nuevo nombre para la lista
existente.

Cortar no es la ú nica forma de clonar listas en Python. La expresió n list(l) devuelve


una copia de la lista l. Si la lista que se va a copiar contiene objetos mutables que
tambié n desea copiar, importe la copia del mó dulo de biblioteca está ndar y use la
funció n copy.deepcopy.

5.2.2 Lista de comprensión


Lista de comprensiónproporciona una manera concisa de aplicar una operació n a
los valores en una secuencia. Crea una nueva lista en la que cada elemento es el
resultado de aplicar una determinada operació n a un valor de una secuencia (por
ejemplo, los elementos de otra lista). Por ejemplo,
L = [x**2 para x en el rango
(1,7)] imprime L
6 Capítulo 5. Tipos estructurados, mutabilidad y funciones de orden

imprimirá la lista
[1, 4, 9, 16, 25, 36]

La clá usula for en una lista de comprensió n puede ir seguida de una o má s


declaraciones if y for que se aplican a los valores producidos por la clá usula for.
Estas clá usulas adicionales modifican la secuencia de valores generada por la
primera clá usula for y producen una nueva secuencia de valores, a la que se aplica la
operació n asociada con la comprensió n.

Por ejemplo, el có digo


mixto = [1, 2, 'a', 3, 4.0]
imprimir [x**2 para x en mixto si tipo(x) == int]

eleva al cuadrado los enterosmezcladoy luego imprime[1, 4, 9].

Algunos programadores de Python usan la comprensió n de listas de maneras


maravillosas y sutiles. Eso no siempre es una gran idea. Recuerde que alguien má s
puede necesitar leer su có digo, y "sutil" no suele ser una propiedad deseable.

5.3 Funciones como objetos


En Python, las funciones son objetos de primera clase. Eso significa que pueden
ser tratados como objetos de cualquier otro tipo, por ejemplo, int o list. Tienen
tipos, por ejemplo, la expresió n tipo(hecho) tiene el valor <tipo 'funció n'>;
pueden aparecer en expresiones, por ejemplo, como el lado derecho de una
instrucció n de asignació n o como argumento de una funció n; pueden ser
elementos de listas; etc.

Usar funciones como argumentos puede ser particularmente conveniente


junto con listas. Permite un estilo de codificació n llamado programació n de
orden superior.
Considere el có digo de la figura 5.5.

Figuraa5.5
def aplicar Aplicar
cada unaf):
uno (L, función a los elementos de una lista
"""Asume que L es una lista, función fa
Muta L reemplazando cada elemento, e, de L por f(e)"""
para i en range(len(L)):
L[i] = f(L[i])

L = [1, -2, 3,33]


imprime 'L =', L
print 'Aplica abs a cada elemento de
L.' aplicar a cada uno (L, abs)
imprime 'L =', L
print 'Aplicar int a cada elemento
de', L applyToEach(L, int)
imprime 'L =', L
print 'Aplicar factorial a cada elemento de', L
applyToEach(L, factR)
imprime 'L =', L
print 'Aplicar Fibonaci a cada elemento de', L
applyToEach(L, fib)
imprime 'L =', L
Capítulo 5. Tipos estructurados, mutabilidad y funciones de orden 6

La funció n aplicarACada se llama de orden superior porque tiene un argumento que es


en sí mismo una funció n. La primera vez que se llama, muta L aplicando la funció n
integrada unaria abs a cada elemento. La segunda vez que se llama, aplica una
conversió n de tipo a cada elemento. La tercera vez que se llama, reemplaza cada
elemento por el resultado de aplicar la funció n factR (definida en la Figura 4.6) a cada
elemento. Y la cuarta vez que se llama, reemplaza cada elemento por el resultado de
aplicar la funció n fib (definida en la Figura 4.7) a cada elemento. se imprime
L = [1, -2, 3,3300000000000001]
Aplicar abs a cada elemento de
L. L = [1, 2,
3.3300000000000001]
Aplicar int a cada elemento de [1, 2, 3.3300000000000001]
L = [1, 2, 3]
Aplicar factorial a cada elemento de [1, 2, 3]
L = [1, 2, 6]
Aplicar Fibonaci a cada elemento de [1, 2, 6]
L = [1, 2, 13]

Python tiene una funció n integrada de orden superior, map, que es similar, pero
má s general, que la funció n applyToEach definida en la Figura 5.5. En su forma má s
simple, el primer argumento para mapear es una funció n unaria (es decir, una
funció n que tiene solo un pará metro) y el segundo argumento es cualquier
colecció n ordenada de valores adecuados como argumentos para el primer
argumento. Devuelve una lista generada aplicando el primer argumento a cada
elemento del segundo argumento. Por ejemplo, la expresió n map(fact, [1, 2, 3])
tiene el valor [1, 2, 6].

De manera má s general, el primer argumento para mapear puede ser una funció n
de n argumentos, en cuyo caso debe ser seguido por n colecciones ordenadas
posteriores. Por ejemplo, el có digo
L1 = [1, 28, 36]
L2 = [2, 57, 9]
imprimir mapa(min, L1, L2)

imprime ellista
[1, 28, 9]
6 Capítulo 5. Tipos estructurados, mutabilidad y funciones de orden

5.4 Cadenas, tuplas y listas


Hemos visto tres tipos de secuencias diferentes: str, tuple y list. Son similares en
el sentido de que se puede operar con objetos de todos estos tipos, como se
describe en la figura 5.6.

Figura
secuencia[i] 5.6 Operaciones
devuelve comunes
la ielelemento en tipos de secuencia
en la secuencia.
len(siguiente)devuelve la longitud de la secuencia.
Algunas de sus otras similitudes y diferencias se resumen en la Figura 5.7.
sec1 + sec2devuelve la concatenació n de las dos secuencias.n
devuelveTipo
* seq.Tipo una de
secuencia que seEjemplos
elementos repite seqde
n literales Mudable
veces.seq[inicio:fin]devuelve un segmento de la
'','a','a B C'
secuencia.
calle caracteres No
e en secuenciaesVerdaderosimiestá contenido en la secuencia yFALSOde
tupla cualquier tipo (), (3,), ('abc', 4) No
lo contrario.e no en secuenciaesVerdaderosimino está en la secuencia
yFALSOdelista cualquier
lo contrario. paratipo
e en seq[],
itera[3],
sobre['abc', 4]
los elementos Síla
de

Figura 5.7 Comparación de tipos de secuencia

Los programadores de Python tienden a usar listas con mucha má s frecuencia que
tuplas. Dado que las listas son mutables, se pueden construir de forma incremental
durante un cá lculo.

Por ejemplo, el có digo siguiente genera de forma incremental una lista que contiene
todos los nú meros pares de otra lista.
evenElems = []
para e en L:
si e%2 == 0:
evenElems.append(e)

Una ventaja de las tuplas es que debido a que son inmutables, el alias nunca es
una preocupació n. Otra ventaja de que sean inmutables es que las tuplas, a
diferencia de las listas, pueden usarse como claves en los diccionarios, como
veremos en la siguiente secció n.
Dado que las cadenas solo pueden contener caracteres, son considerablemente
menos versá tiles que las tuplas o las listas. Por otro lado, cuando trabaja con una
cadena de caracteres, existen muchos mé todos integrados que facilitan la vida. La
figura 5.8 contiene breves descripciones de algunos de ellos. Tenga en cuenta que,
dado que las cadenas son inmutables, todas ellas devuelven valores y no tienen
efectos secundarios.
Capítulo 5. Tipos estructurados, mutabilidad y funciones de orden 6

s.count(s1)cuenta cuantas veces la cadenas1ocurre ens.


para encontrar (s1)devuelve el índice de la primera aparició n de la subcadena
s1 en
s, y-1sis1no está dentros.
s.rfind(s1)igual queencontrar, pero comienza desde el final des(el "r"
enencontrar
significa reverso).
s.índice(s1)igual queencontrar, pero plantea una excepció n (ver Capítulo 7)
sis1no está dentros.
s.rindex(s1)igual queíndice, pero comienza desde el final des.
Más lento()convierte todas las letras mayú sculas en s a minú sculas.
s.replace(antiguo, nuevo)reemplaza todas las apariciones de la
cadenaviejoenscon la cuerdanuevo.
s.rstrip()elimina el espacio en blanco final des.
s.split(d)divisionessusandodcomo delimitador. Devuelve una lista de
subcadenas des. Por ejemplo, el valor de'David Guttag juega
baloncesto'.split(' ')es['David', 'Guttag', 'juegos',
'baloncesto'].Sidse omite, las subcadenas se separan por cadenas arbitrarias
Figura 5.8 Algunos métodos en cadenas

5.5 Diccionarios
Los objetos de tipo dict (abreviatura de diccionario) son como listas, excepto que los
"índices" no necesitan ser nú meros enteros; pueden ser valores de cualquier tipo
inmutable. Como no está n ordenados, los llamamos claves en lugar de índices.
Piense en un diccionario como un conjunto de pares clave/valor. Los literales de
tipo dict se encierran entre llaves y cada elemento se escribe como una clave
seguida de dos puntos seguidos de un valor.

Por ejemplo, el có digo,


numeromes = {'Ene':1, 'Feb':2, 'Mar':3, 'Abr':4, 'May':5,
1: 'Ene', 2: 'Feb', 3: 'Mar', 4: 'Abr', 5: 'May'}
print 'El tercer mes es ' + numeromes[3] dist =
numeromes['abril'] - numeromes['ene'] print
'abril y enero son', dist, 'meses separados'

imprimirá
el tercer mes es mar
abril y enero tienen 3 meses de diferencia

Las entradas en un dictado está n desordenadas y no se puede acceder a ellas con


un índice. Es por eso que MonthNumbers[1] se refiere sin ambigü edades a la
entrada con la clave 1 en lugar de a la segunda entrada.

El mé todo keys devuelve una lista que contiene las claves de un diccionario. El
orden en que aparecen las teclas no está definido. Así, por ejemplo, el có digo
imprimir numerosmes.keys()podría imprimir

[1, 2, 'Mar', 'Feb', 5, 'Abr', 'Ene', 'May', 3, 4]


6 Capítulo 5. Tipos estructurados, mutabilidad y funciones de orden

Cuando se usa una instrucció n for para iterar sobre un diccionario, el valor asignado
a la variable de iteració n es una clave, no un par clave/valor. Por ejemplo, el có digo
claves = []
for e in monthNumbers:
keys.append(e)
keys.sort()
imprimir
claves

huellas dactilares[1, 2, 3, 4, 5, 'Abr', 'Feb', 'Ene', 'Mar', 'May'].

Los diccionarios son una de las mejores cosas de Python. Reducen en gran medida la
dificultad de escribir una variedad de programas. Por ejemplo, en la Figura 5.9
usamos diccionarios para escribir un programa (bastante horrible) para traducir
entre idiomas.

EtoF = {'pan':'dolor', 'vino':'vin', 'con':'avec', 'yo':'je',


'comer':'sarna', 'beber':'bois', 'John':'Jean',
'amigos':'amis', 'y': 'et', 'de':'du','red':'rouge'}
FtoE = {'dolor':'pan', 'vin':'vino', 'avec':'con', 'Je':'I',
'mange':'comer', 'bois':'beber', 'Jean':'John',
'amis':'friends', 'et':'and', 'du':'of', 'rouge':'red'}
dicts = {'Inglés a francés':EtoF, 'Francés a inglés':FtoE }

def traducirPalabra(palabra,
diccionario): if palabra en
diccionario.teclas():
return
diccionario[palabra] elif
palabra != '':
devolver '"' + palabra
+ '"' devolver palabra

def traducir (frase, dictados, dirección):


UCLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
LCLetters = 'abcdefghijklmnopqrstuvwxyz'
letras = UCLetters + LCLetters
diccionario = dicts[dirección]
traducción = ''
palabra = ''
para c en la frase:
si c en letras:
palabra =
palabra + c
demás:
traducción = traducción\
+ traducirPalabra(palabra, diccionario) + c
palabra = ''
volver traducción + ' ' + traducirPalabra(palabra, diccionario)

print translate('Bebo buen vino tinto y como pan.', dicts,'English


to French')
print translate('Je bois du vin rouge.',
dictados, 'francés a inglés')

Figura 5.9 Traducir texto (mal)

El có digo en la figura imprime,

Je bois "bueno" rouge vin, et sarna


dolor. Bebo de vino tinto.

Al igual que las listas, los diccionarios son mutables. Por lo tanto, hay que tener
cuidado con los efectos secundarios. Por ejemplo,
Capítulo 5. Tipos estructurados, mutabilidad y funciones de orden 6

FtoE['bois'] = 'madera'
print translate('Je bois du vin rouge.', dicts, 'Francés a Inglés')

imprimirá
Madera de vino tinto.

Agregamos elementos a un diccionario asignando un valor a una clave no utilizada, por


ejemplo,
FtoE['blanc'] = 'blanco'

Al igual que con las listas, existen muchos mé todos ú tiles, incluidos algunos para
eliminar elementos, asociados con los diccionarios. No los enumeramos aquí,
pero los usaremos segú n convenga en ejemplos má s adelante en el libro. La
figura 5.10 contiene algunas de las operaciones má s ú tiles en los diccionarios.

Figura 5.10
prestar)devuelve el núAlgunas
mero deoperaciones
elementos encomunes
d. en dictados
d.teclas()devuelve una lista que contiene las claves
Los objetos de cualquier tipo inmutable, por ejemplo, tipo tupla, pueden usarse como
end.de
claves d.valores()devuelve una lista que contiene los
diccionario. Imagine, por ejemplo, usar una tupla de la forma
valores end.amable
(flightNumber, devoluciones
day) para Verdadero
representar si clave
vuelos de líneaskes
aéreas. Entonces sería fá cil
usarentales
d. tuplas como claves en un diccionario que implemente un mapeo desde los
devuelve
d[k]hasta
vuelos el artículo
los tiempos deen dcon llavek.
llegada.
d.obtener(k, v)devolucionesd[k]sikes end, yvde lo contrario.
La mayoría de los lenguajes de programació n no contienen un tipo incorporado
d[k] = vasocia el valor v con la clave k en d. Si ya hay un valor asociado con k,
que proporcione una asignació n de claves a valores. En su lugar, los
ese valor se reemplaza.
programadores utilizan otros tipos para proporcionar una funcionalidad similar.
del d[k]quita la llavekded.
Es, por ejemplo, relativamente fá cil implementar un diccionario utilizando una
lista en la que cada elemento es un par clave/valor. Entonces se puede escribir
una funció n simple que hace la recuperació n asociativa, por ejemplo,
def keySearch(L, k):
for elem in L:
si elemento[0] ==
k: devuelve
elemento[1]
volver Ninguno

El problema con tal implementació n es que es computacionalmente ineficiente. En


el peor de los casos, un programa podría tener que examinar cada elemento de la
lista para realizar una sola recuperació n. Por el contrario, la implementació n
incorporada es bastante rá pida. Utiliza una té cnica llamada hash, descrita en el
Capítulo 10, para realizar la bú squeda en el tiempo que es casi independiente del
tamañ o del diccionario.
6PRUEBAS Y DEPURACIÓN

Odiamos mencionar esto, pero el Dr. Pangloss estaba equivocado. No vivimos en “el
mejor de los mundos posibles”. Hay algunos lugares donde llueve muy poco y otros
donde llueve demasiado. Algunos lugares son demasiado fríos, otros demasiado
calurosos y otros demasiado calurosos en verano y demasiado fríos en invierno. A
veces, el mercado de valores baja mucho. Y, quizá s lo peor de todo, nuestros
programas no siempre funcionan correctamente la primera vez que los ejecutamos.

Se han escrito libros sobre có mo lidiar con este ú ltimo problema, y hay mucho que
aprender leyendo estos libros. Sin embargo, con el interé s de brindarle algunos
consejos que podrían ayudarlo a resolver el pró ximo problema a tiempo, este
capítulo proporciona una discusió n muy condensada del tema. Si bien todos los
ejemplos de programació n está n en Python, los principios generales son aplicables
para lograr que cualquier sistema complejo funcione.

Pruebases el proceso de ejecutar un programa para tratar de determinar si


funciona o no segú n lo previsto. La depuració n es el proceso de intentar reparar un
programa que ya sabe que no funciona como se esperaba.

La prueba y la depuració n no son procesos en los que deba comenzar a pensar


después de que se haya creado un programa. Los buenos programadores diseñ an
sus programas de manera que sean má s fá ciles de probar y depurar. La clave para
hacer esto es dividir el programa en componentes separados que se pueden
implementar, probar y depurar independientemente de otros componentes. En este
punto del libro, hemos discutido solo un mecanismo para modularizar programas, la
funció n.
Entonces, por ahora, todos nuestros ejemplos se basará n en funciones. Cuando
lleguemos a otros mecanismos, en clases particulares, volveremos a algunos de los
temas tratados en este capítulo.

El primer paso para lograr que un programa funcione es hacer que el sistema de
lenguaje acepte ejecutarlo, es decir, eliminar los errores de sintaxis y los errores
semá nticos está ticos que pueden detectarse sin ejecutar el programa. Si no ha
superado ese punto en su programació n, no está listo para este capítulo. Dedique un
poco má s de tiempo a trabajar en pequeñ os programas y luego regrese.

6.1 Pruebas
Lo má s importante que decir acerca de las pruebas es que su propó sito es mostrar
que existen errores, no mostrar que un programa está libre de errores. Para citar a
Edsger Dijkstra: “Las pruebas de programas se pueden usar para mostrar la
presencia de errores, ¡pero nunca para mostrar su ausencia!” un solo experimento
puede probar que estoy equivocado.

29“Notassobre la programació n estructurada”, Universidad Té cnica de Eindhoven,


Informe TH 70-WSK-03, abril de 1970.
Capítulo 6. Pruebas y 7

¿Por qué esto es tan? Incluso el má s simple de los programas tiene miles de
millones de entradas posibles. Considere, por ejemplo, un programa que pretende
cumplir con la especificació n:
def es más grande (x, y):
"""Asume que x e y son números enteros
Devuelve True si x es menor que y y False en caso contrario."""

Ejecutarlo en todos los pares de enteros sería, por decir lo menos, tedioso. Lo
mejor que podemos hacer es ejecutarlo en pares de enteros que tengan una
probabilidad razonable de producir una respuesta incorrecta si hay un error en el
programa.

La clave para las pruebas es encontrar una colecció n de entradas, denominada


conjunto de pruebas, que tenga una alta probabilidad de revelar errores, pero
que no tarde demasiado en ejecutarse. La clave para hacer esto es dividir el
espacio de todas las entradas posibles en subconjuntos que proporcionen
informació n equivalente sobre la correcció n del programa y luego construir un
conjunto de pruebas que contenga una entrada de cada partició n. (Por lo general,
la construcció n de un conjunto de pruebas de este tipo no es realmente posible.
Piense en esto como un ideal inalcanzable).

Una partició n de un conjunto divide ese conjunto en una colecció n de


subconjuntos de manera que cada elemento del conjunto original pertenece
exactamente a uno de los subconjuntos. Considere, por ejemplo, isBigger(x, y). El
conjunto de entradas posibles son todas las combinaciones de pares de enteros.
Una forma de dividir este conjunto es en estos siete subconjuntos:

 Xpositivo,ypositivo
 Xnegativo,ynegativo
 Xpositivo,ynegativo
 Xnegativo,ypositivo
 x = 0, y = 0
 x = 0,y≠0
 X≠0,y = 0

Si uno probara la implementació n en al menos un valor de cada uno de estos


subconjuntos, habría una probabilidad razonable (pero no garantía) de exponer un
error, si existe.

Para la mayoría de los programas, encontrar una buena partició n de las


entradas es mucho má s fá cil decirlo que hacerlo. Por lo general, las personas
confían en la heurística basada en la exploració n de diferentes caminos a travé s
de alguna combinació n del có digo y las especificaciones. Las heurísticas
basadas en la exploració n de rutas a travé s del có digo pertenecen a una clase
llamada prueba de caja de cristal. Las heurísticas basadas en la exploració n de
caminos a travé s de la especificació n caen en una clase llamada prueba de caja
negra.

6.1.1 Pruebas de caja negra


En principio, las pruebas de caja negra se construyen sin mirar el có digo que se va
a probar. Las pruebas de caja negra permiten que los evaluadores e
implementadores provengan de poblaciones separadas. Cuando aquellos de
nosotros que enseñ amos cursos de programació n generamos casos de prueba
para los conjuntos de problemas que asignamos a los estudiantes, estamos
7 Capítulo 6. Pruebas y
desarrollando conjuntos de pruebas de caja negra. Los desarrolladores de
software comercial suelen tener grupos de control de calidad que son en gran
medida independientes de los grupos de desarrollo.
Capítulo 6. Pruebas y 7

Esta independencia reduce la probabilidad de generar conjuntos de pruebas que


muestren errores que se correlacionan con errores en el có digo. Supongamos, por
ejemplo, que el autor de un programa hizo la suposició n implícita, pero invá lida, de
que nunca se llamaría a una funció n con un nú mero negativo. Si la misma persona
construyó el conjunto de pruebas para el programa, probablemente repetiría el
error y no probaría la funció n con un argumento negativo.

Otra característica positiva de las pruebas de caja negra es que es só lida con
respecto a los cambios de implementació n. Dado que los datos de prueba se
generan sin conocimiento de la implementació n, no es necesario cambiarlos
cuando se cambia la implementació n.

Como dijimos anteriormente, una buena manera de generar datos de prueba de caja
negra es explorar caminos a travé s de una especificació n. Considere, la especificació n
def sqrt(x, epsilon): """Asume
x, epsilon flota
x >= 0
epsilon > 0
Devuelve un resultado
tal que
x-epsilon <= resultado*resultado <= x+epsilon"""

Parece que solo hay dos caminos distintos a través de esta especificació n: uno
correspondiente a x = 0 y otro correspondiente a x > 0. Sin embargo, el sentido
comú n nos dice que si bien es necesario probar estos dos casos, no es
suficiente.

También se deben probar las condiciones de contorno. Al mirar listas, esto a


menudo significa mirar la lista vacía, una lista con exactamente un elemento y una
lista que contiene listas. Cuando se trata de nú meros, generalmente significa mirar
valores muy pequeñ os y muy grandes, así como valores "típicos". Para sqrt, podría
tener sentido probar valores de x y epsilon similares a los de la siguiente tabla.

Las primeras cuatro filas está n destinadas a X épsilon


representar casos típicos. Observe que los valores de x
0.0 0.0001
incluyen un perfecto
cuadrado, un nú mero menor que uno y un nú mero 25,0 0.0001
con una raíz cuadrada irracional. Si alguna de estas 0.5 0.0001
pruebas falla, hay un error en el programa que debe 2.0 0.0001
corregirse.
2.0 1,0/2,0**64,0
Las filas restantes prueban valores extremadamente 1,0/2,0**64 1,0/2,0**64,0
grandes y pequeñ os de x y épsilon. Si alguna de estas
2,0**64,0 1,0/2,0**64,0
pruebas falla, hay que arreglar algo. Tal vez haya un
error en el có digo que deba corregirse, o tal vez deba 1,0/2,0**64,0 2,0**64,0
cambiarse la especificació n para que sea má s fá cil de 2,0**64,0 2,0**64,0
cumplir. Podría, por ejemplo, no ser razonable
esperar encontrar una aproximació n de una raíz
cuadrada cuando é psilon es ridículamente pequeñ o.
7 Capítulo 6. Pruebas y

Otra condició n límite importante en la que pensar es el aliasing. Consideremos, por


ejemplo, el có digo
copia predeterminada (L1, L2):
"""Asume que L1, L2 son listas
Muta L2 para que sea una copia de L1"""
while len(L2) > 0: #eliminar todos los elementos
de L2 L2.pop() #eliminar el último elemento
de L2
para e en L1: #agregar los elementos de L1 para vaciar
inicialmente L2 L2.append(e)

Funcionará la mayor parte del tiempo, pero no cuando L1 y L2 se refieran a la


misma lista. Cualquier conjunto de pruebas que no incluyera una llamada de la
copia del formulario (L, L), no revelaría el error.

6.1.2 Pruebas de caja de vidrio


Las pruebas de caja negra nunca deben omitirse, pero rara vez son suficientes.
Sin mirar la estructura interna del có digo, es imposible saber qué casos de
prueba es probable que proporcionen nueva informació n. Considere el siguiente
ejemplo trivial:
def esPrimo(x):
"""Asume que x es un int no negativo
Devuelve True si x es primo; Falso de lo
contrario """ si x <= 2:
falso retorno
para i en rango(2,
x): si x%i == 0:
volver Falso
volver Verdadero

Mirando el có digo, podemos ver que debido a la prueba si x <= 2, los valores 0, 1 y
2 se tratan como casos especiales y, por lo tanto, deben probarse. Sin mirar el
có digo, uno podría no probar isPrime(2) y, por lo tanto, no descubriría que la
llamada a la funció n isPrime(2) devuelve False, lo que indica erró neamente que 2
no es un nú mero primo.

Los conjuntos de pruebas de caja de vidrio suelen ser mucho má s fá ciles de


construir que los conjuntos de pruebas de caja negra. Las especificaciones suelen
ser incompletas y, a menudo, bastante descuidadas, lo que hace que sea un
desafío estimar cuá n minuciosamente un conjunto de pruebas de caja negra
explora el espacio de entradas interesantes. Por el contrario, la noció n de una
ruta a travé s del có digo está bien definida y es relativamente fá cil evaluar qué tan
a fondo se está explorando el espacio. De hecho, existen herramientas
comerciales que se pueden utilizar para medir objetivamente la integridad de las
pruebas de caja de vidrio.

Un conjunto de pruebas de caja de vidrio tiene una ruta completa si ejercita todas
las rutas potenciales a travé s del programa. Por lo general, esto es imposible de
lograr, ya que depende de la cantidad de veces que se ejecuta cada ciclo y la
profundidad de cada recursió n. Por ejemplo, una implementació n recursiva de
factorial sigue un camino diferente para cada entrada posible (porque el nú mero de
niveles de recursividad diferirá ).
Capítulo 6. Pruebas y 7

Ademá s, incluso un conjunto de pruebas de ruta completa no garantiza que se


expongan todos los errores. Considerar:
definición abs(x):
"""Asume que x es un int
Devuelve x si x>=0 y –x en caso
contrario""" si x < -1:
volver -x
más:
volver x

La especificació n sugiere que hay dos casos posibles, x es negativo o no lo es. Esto
sugiere que el conjunto de entradas {2, -2} es suficiente para explorar todas las rutas
de la especificació n. Este conjunto de pruebas tiene la agradable propiedad adicional
de forzar el programa a través de todas sus rutas, por lo que también parece un
conjunto completo de cajas de cristal. El ú nico problema es que este conjunto de
pruebas no expondrá el hecho de que abs(-1) devolverá -1.

A pesar de las limitaciones de las pruebas en caja de vidrio, existen algunas reglas
generales que generalmente vale la pena seguir:

 Ejercer ambas ramas de todossideclaraciones.


 Asegú rese de que cadaexceptose ejecuta la clá usula (ver Capítulo 7).
 Para cadaparabucle, tener casos de prueba en los que
o El ciclo no se ingresa (por ejemplo, si el ciclo itera sobre los
elementos de una lista, asegú rese de que se prueba en la lista
vacía),
o El cuerpo del bucle se ejecuta exactamente una vez, y
o El cuerpo del bucle se ejecuta má s de una vez.
 Para cadamientrasbucle,
o Mire los mismos tipos de casos que cuando se trata de parabucles, y
o Incluya casos de prueba correspondientes a todas las formas
posibles de salir del bucle. Por ejemplo, para un ciclo que
comienza con
mientras que len(L) > 0 y no L[i] == e
encuentre casos en los que el bucle sale porque len(L) es mayor que
cero y casos en los que sale porque L[i] == e.
 Para funciones recursivas, incluya casos de prueba que hagan que la
funció n regrese sin llamadas recursivas, exactamente una llamada
recursiva y má s de una llamada recursiva.

6.1.3 Realización de pruebas


A menudo se piensa que las pruebas ocurren en dos fases. Siempre se debe
comenzar con las pruebas unitarias. Durante esta fase, los evaluadores construyen y
ejecutan pruebas diseñ adas para determinar si las unidades individuales de có digo
(p. ej., funciones) funcionan correctamente.
A esto le sigue la prueba de integració n, que está diseñ ada para determinar si el
programa en su conjunto se comporta segú n lo previsto. En la prá ctica, los
evaluadores recorren
7 Capítulo 6. Pruebas y

estas dos fases, ya que las fallas durante las pruebas de integració n conducen a
realizar cambios en las unidades individuales.

Las pruebas de integració n son casi siempre má s desafiantes que las pruebas
unitarias. Una de las razones de esto es que el comportamiento previsto de un
programa completo suele ser considerablemente má s difícil de caracterizar que el
comportamiento previsto de cada una de sus partes. Por ejemplo, caracterizar el
comportamiento previsto de un procesador de textos es considerablemente má s
desafiante que caracterizar el comportamiento de una funció n que cuenta la
cantidad de caracteres en un documento. Los problemas de escala tambié n pueden
dificultar las pruebas de integració n. No es raro que las pruebas de integració n
tarden horas o incluso días en ejecutarse.

Muchas organizaciones de desarrollo de software industrial tienen un grupo de


aseguramiento de la calidad del software (SQA) que está separado del grupo
encargado de implementar el software. La misió n de este grupo es asegurarse de
que antes de que se publique el software, sea adecuado para el propó sito previsto.
En algunas organizaciones, el grupo de desarrollo es responsable de las pruebas
unitarias y el grupo de control de calidad de las pruebas de integració n.

En la industria, el proceso de prueba suele estar altamente automatizado. Los


probadores30 no se sientan en las terminales escribiendo entradas y
comprobando salidas. En su lugar, utilizan controladores de prueba que de forma
autó noma

 Configurar el entorno necesario para invocar el programa (o unidad) a


probar,
 Invocar el programa (o unidad) a probar con una secuencia de
entradas predefinida o generada automá ticamente,
 Guarda los resultados de estas invocaciones,
 Verificar la aceptabilidad de los resultados de las pruebas, y
 Preparar un informe adecuado.

Durante las pruebas unitarias, a menudo necesitamos crear stubs ademá s de


controladores. Los controladores simulan partes del programa que usa la unidad
que se está probando, mientras que los stubs simulan partes del programa que
usa la unidad que se está probando. Los stubs son ú tiles porque permiten a las
personas probar unidades que dependen del software o, a veces, incluso del
hardware que aú n no existe. Esto permite que los equipos de programadores
desarrollen y prueben simultá neamente mú ltiples partes de un sistema.

Idealmente, un taló n debería

 Verifique la razonabilidad del entorno y los argumentos proporcionados


por la persona que llama (llamar a una funció n con argumentos
inapropiados es un error comú n),
 Modificar argumentos y variables globales de manera consistente con la
especificació n, y
 Devolver valores consistentes con la especificació n.

30O,para el caso, aquellos que califican conjuntos de problemas en cursos de programació n muy
extensos.
Capítulo 6. Pruebas y 7

La creació n de talones adecuados suele ser un desafío. Si la unidad que reemplaza el


stub está destinada a realizar alguna tarea compleja, crear un stub que realice
acciones consistentes con la especificació n puede ser equivalente a escribir el
programa que el stub está diseñ ado para reemplazar. Una forma de superar este
problema es limitar el conjunto de argumentos aceptados por el stub y crear una
tabla que contenga los valores que se devolverá n para cada combinació n de
argumentos que se utilizará en el conjunto de pruebas.

Una atracció n de la automatizació n del proceso de prueba es que facilita la prueba


de regresió n. Cuando los programadores intentan depurar un programa, es muy
comú n instalar una "reparació n" que rompe algo que solía funcionar. Siempre que
realice algú n cambio, por pequeñ o que sea, debe comprobar que el programa sigue
superando todas las pruebas que solía pasar.

6.2 depuración
Hay una encantadora leyenda urbana sobre có mo el proceso de corregir fallas
en el software llegó a conocerse como depuració n. La foto de abajo es de una
pá gina del 9 de septiembre de 1947 en un libro de laboratorio del grupo que
trabaja en la calculadora de relé s Mark II Aiken en la Universidad de Harvard.

Algunos han afirmado que el descubrimiento de esa desafortunada polilla atrapada


en el Mark II condujo al uso de la frase depuració n. Sin embargo, la redacció n,
"Primer caso real de error encontrado", sugiere que una interpretació n menos
literal de la frase ya era comú n. Grace Murray Hopper, líder del proyecto Mark II,
dejó en claro que el té rmino "error" ya se usaba ampliamente para describir
problemas con los sistemas electró nicos durante la Segunda Guerra Mundial. Y
mucho antes de eso, el Nuevo Catecismo de Electricidad de Hawkins, un manual
elé ctrico de 1896, incluía la entrada: "El té rmino 'error' se usa de forma limitada
para designar cualquier falla o problema en las conexiones o el funcionamiento de
los aparatos eléctricos". En el uso del inglé s, la palabra "bugbear" significa
"cualquier cosa que cause aparentemente innecesaria o excesiva
7 Capítulo 6. Pruebas y

miedo o ansiedad”. 31 Shakespeare parece haber abreviado esto a “bicho”,


cuando hizo que Hamlet se quejara sobre “bichos y duendes en mi vida”. 32

El uso de la palabra "error" a veces lleva a las personas a ignorar el hecho


fundamental de que si escribiste un programa y tiene un "error", te equivocaste.
Los errores no se arrastran espontá neamente en programas impecables. Si tu
programa tiene un bug, es porque lo pusiste ahí. Los errores no se reproducen en
los programas. Si su programa tiene varios errores, es porque cometió varios
errores.

Los errores de tiempo de ejecució n se pueden clasificar en dos dimensiones:

1. Abierto encubierto: Un error manifiesto tiene una manifestació n


obvia, por ejemplo, el programa falla o tarda mucho má s (quizá s una
eternidad) en ejecutarse de lo que debería. Un error encubierto no tiene
una manifestació n obvia. El programa puede ejecutarse hasta la
conclusió n sin ningú n problema, aparte de proporcionar una respuesta
incorrecta. Muchos errores se encuentran entre los dos extremos, y si el
error es manifiesto o no puede depender de cuá n cuidadosamente se
examine el comportamiento del programa.
2. Persistente intermitente: Se produce un error persistente cada vez
que se ejecuta el programa con las mismas entradas. Un error
intermitente ocurre solo algunas veces, incluso cuando el programa se
ejecuta en las mismas entradas y aparentemente bajo las mismas
condiciones. Cuando lleguemos al Capítulo 12, comenzaremos a escribir
programas donde los errores intermitentes son comunes.
Los mejores tipos de errores que se pueden tener son manifiestos y persistentes.
Los desarrolladores no pueden hacerse ilusiones sobre la conveniencia de
implementar el programa. Y si alguien má s es lo suficientemente tonto como
para intentar usarlo, rá pidamente descubrirá su locura. Quizá s el programa haga
algo horrible antes de colapsar, por ejemplo, borrar archivos, pero al menos el
usuario tendrá motivos para estar preocupado (si no entrado en pá nico). Los
buenos programadores intentan escribir sus programas de tal manera que los
errores de programació n conduzcan a fallos que son manifiestos y persistentes.
Esto a menudo se llama programació n defensiva.

El siguiente paso hacia el foso de la indeseabilidad son los errores que son
manifiestos pero intermitentes. Un sistema de control de trá fico aé reo que calcule la
ubicació n correcta de los aviones casi todo el tiempo sería mucho má s peligroso que
uno que cometa errores obvios todo el tiempo. Uno puede vivir en el paraíso de los
tontos por un período de tiempo, y tal vez llegar tan lejos como implementar un
sistema que incorpore el programa defectuoso, pero tarde o temprano el error se
manifestará . Si las condiciones que provocan que el error se manifieste son
fá cilmente reproducibles, a menudo es relativamente fá cil localizar y reparar el
problema. Si las condiciones que provocan el error no está n claras, la vida es mucho
má s difícil.

Los programas que fallan de forma encubierta suelen ser muy peligrosos. Dado
que aparentemente no son problemá ticos, las personas los usan y confían en que
hará n lo correcto. Cada vez má s, la sociedad confía en el software para realizar
cá lculos críticos que está n má s allá de la capacidad de los humanos para
realizarlos o incluso verificar su correcció n.

31Diccionario universitario del Nuevo Mundo de Webster.


32Acto 5, escena 2.
Capítulo 6. Pruebas y 7

Por lo tanto, un programa puede proporcionar una respuesta falaz no detectada


durante largos períodos de tiempo. Dichos programas pueden, y han causado, mucho
dañ o.33 Un programa que evalú a el riesgo de una cartera de bonos hipotecarios y
escupe confiadamente la respuesta incorrecta puede causar muchos problemas a un
banco (y quizá s a toda la sociedad). Una má quina de radioterapia que administra un
poco má s o un poco menos de radiació n de lo previsto puede ser la diferencia entre
la vida y la muerte para una persona con cá ncer. Un programa que comete un error
encubierto só lo ocasionalmente puede o no causar menos estragos que uno que
siempre comete dicho error. Los errores que son tanto encubiertos como
intermitentes son casi siempre los má s difíciles de encontrar y corregir.

6.2.1 Aprendiendo a depurar


La depuració n es una habilidad aprendida. Nadie lo hace bien instintivamente.
La buena noticia es que no es difícil de aprender y es una habilidad transferible.
Las mismas habilidades que se usan para depurar el software se pueden usar
para descubrir qué está mal con otros sistemas complejos, por ejemplo,
experimentos de laboratorio o humanos enfermos.

Durante al menos cuatro décadas, la gente ha estado construyendo herramientas


llamadas depuradores, y hay herramientas de depuració n integradas en IDLE. Se
supone que ayudan a las personas a encontrar errores en sus programas. Pueden
ayudar, pero solo un poco. Lo que es mucho má s importante es có mo abordas el
problema. Muchos programadores experimentados ni siquiera se molestan con las
herramientas de depuració n. La mayoría de los programadores dicen que la
herramienta de depuració n má s importante es la declaració n de impresió n.

La depuració n comienza cuando las pruebas han demostrado que el programa se


comporta de manera no deseada. La depuració n es el proceso de buscar una
explicació n de ese comportamiento. La clave para ser consistentemente bueno en
la depuració n es ser sistemá tico al realizar esa bú squeda.

Comience por estudiar los datos disponibles. Esto incluye los resultados de la
prueba y el texto del programa. Estudia todos los resultados de las pruebas.
Examine no solo las pruebas que revelaron la presencia de un problema, sino
tambié n aquellas pruebas que parecían funcionar perfectamente. Tratar de
entender por qué una prueba funcionó y otra no, a menudo es esclarecedor. Al
mirar el texto del programa, tenga en cuenta que no lo entiende completamente.
Si lo hiciera, probablemente no habría un error.

Luego, formula una hipó tesis que creas que es consistente con todos los datos. La
hipó tesis podría ser tan limitada como "si cambio la línea 403 de x < y a x <= y, el
problema desaparecerá " o tan amplia como "mi programa no termina porque tengo
la condició n de salida incorrecta en algú n ciclo while". .”

Luego, diseñ e y ejecute un experimento repetible con el potencial de refutar la


hipó tesis. Por ejemplo, puede colocar una declaració n de impresió n antes y despué s
de cada bucle while. Si estos siempre está n emparejados, entonces se ha refutado la
hipó tesis de que un ciclo while está causando la no terminació n. Decida antes de
ejecutar el experimento có mo interpretaría varios resultados posibles. Si esperas
hasta

33El1 de agosto de 2012, Knight Capital Group, Inc. implementó una nueva pieza de
software de negociació n de acciones. En cuarenta y cinco minutos, un error en ese
software hizo perder a la empresa
$440,000,000. Al día siguiente, el CEO de Knight comentó que el error provocó que el
8 software ingresara “una tonelada de pedidos, todos erró neos”.
Capítulo 6. Pruebas y
Capítulo 6. Pruebas y 8

después de ejecutar el experimento, es má s probable que caiga presa de ilusiones.

Finalmente, es importante mantener un registro de los experimentos que ha

realizado.
Cuando ha pasado muchas horas cambiando su có digo tratando de rastrear un error
esquivo, es fá cil olvidar lo que ya ha intentado. Si no tiene cuidado, es fá cil perder
demasiadas horas intentando el mismo experimento (o má s probablemente un
experimento que parece diferente pero le dará la misma informació n) una y otra
vez. Recuerde, como muchos han dicho, “locura es hacer lo mismo, una y otra vez,
pero esperando resultados diferentes.”34

6.2.2 Diseñando el Experimento


Piense en la depuració n como un proceso de bú squeda y en cada experimento como
un intento de reducir el tamañ o del espacio de bú squeda. Una forma de reducir el
tamañ o del espacio de bú squeda es diseñ ar un experimento que pueda usarse para
decidir si una regió n específica del có digo es responsable de un problema
descubierto durante las pruebas de integració n. Otra forma de reducir el espacio de
bú squeda es reducir la cantidad de datos de prueba necesarios para provocar la
manifestació n de un error.

Veamos un ejemplo artificial para ver có mo se podría depurar. Imagine que escribió
el có digo de verificació n del palíndromo en la figura 6.1 y que está tan seguro de sus
habilidades de programació n que lo publica en la Web, sin probarlo. Suponga
ademá s que recibe un correo electró nico que dice: “¡¡Probé su !!**! programa en la
siguiente entrada de 1000 cadenas, e imprimió Sí. Sin embargo, cualquier tonto
puede ver que no es un palíndromo. ¡Arreglalo!"

Figura 6.1 Programa con errores


def esAmigo(x):
"""Asume que x es una lista
Devuelve True si la lista es un palíndromo; Falso de lo
34Esta línea aparece en Sudden Death de Rita Mae Brown. Sin embargo, se ha atribuido de
contrario """ temp = x
diversastemp.revers
formas a muchas otras fuentes, incluido Albert Einstein.
e si temp
== x:
devolver
Verdadero
más:
falso retorno

def tonto(n):
"""Supone que n es un
int > 0 Obtiene n
entradas del usuario
Imprime 'Sí' si la secuencia de entradas forma un
palíndromo; 'No' de lo contrario"""
para i en el
rango (n):
resultado =
[]
elem = raw_input('Ingrese el
8 Capítulo 6. Pruebas y

Puede intentarlo y probarlo en la entrada de 1000 cadenas suministrada. Pero


podría ser má s sensato comenzar probá ndolo en algo má s pequeñ o. De hecho,
tendría sentido probarlo en un no palíndromo mínimo, por ejemplo,
>>> tonto(2)
Ingrese
elemento: a
Ingrese
elemento: b

La buena noticia es que falla incluso en esta simple prueba, por lo que no tiene
que escribir mil cadenas. La mala noticia es que no tienes idea de por qué falló .

En este caso, el có digo es lo suficientemente pequeñ o como para que puedas mirarlo
y encontrar el error (o errores). Sin embargo, supongamos que es demasiado
grande para hacer esto y comencemos a reducir sistemá ticamente el espacio de
bú squeda.

A menudo, la mejor manera de hacer esto es realizar una bú squeda binaria.


Encuentre algú n punto aproximadamente a la mitad del có digo e idee un
experimento que le permita decidir si hay un problema antes de ese punto que
podría estar relacionado con el síntoma. (Por supuesto, tambié n puede haber
problemas después de ese punto, pero generalmente es mejor buscar un problema a
la vez.) Al elegir ese punto, busque un lugar donde haya algunos valores intermedios
fá ciles de examinar que proporcionen informació n. Si un valor intermedio no es lo
que esperaba, probablemente haya un problema que ocurrió antes de ese punto en
el có digo. Si todos los valores intermedios se ven bien, el error probablemente se
encuentre en algú n lugar má s adelante en el có digo. Este proceso se puede repetir
hasta que haya reducido la regió n en la que se encuentra un problema a unas pocas
líneas de có digo.

Mirando tonto, el punto medio está alrededor de la línea si esPal (resultado). Lo


obvio que hay que comprobar es si el resultado tiene el valor esperado, ['a', 'b'].
Verificamos esto insertando el resultado de la impresió n de la declaració n antes de la
declaració n if en tonto. Cuando se ejecuta el experimento, el programa imprime ['b'],
lo que sugiere que algo ya salió mal. El siguiente paso es imprimir el resultado
aproximadamente a la mitad del ciclo. Esto revela rá pidamente que result nunca
tiene má s de un elemento de longitud, lo que sugiere que la inicializació n de result
debe moverse fuera del bucle for. El có digo corregido para tonto es
def tonto(n):
"""Supone que n es un int
> 0 Obtiene n entradas
del usuario
Imprime 'Sí' si la secuencia de entradas forma un
palíndromo; 'No' de lo contrario"""
resultado = []
para i en el rango (n):
elem = raw_input('Ingrese el
elemento:') resultado.append(elem)
resultado de impresión
if
isPal(resulta
do): imprime
'Sí'
demás:
imprime 'No'

Intenté moslo y veamos si el resultado tiene el valor correcto despué s del ciclo for.
Lo hace, pero desafortunadamente el programa todavía imprime Sí. Ahora, tenemos
razones para creer que hay un segundo error debajo de la declaració n de
Capítulo 6. Pruebas y 8
impresió n. Entonces, echemos un vistazo a isPal. La línea if temp == x: está
aproximadamente a la mitad de esa funció n. Entonces, insertamos el
8 Capítulo 6. Pruebas y

líneatemperatura de impresión, xantes de esa línea. Cuando ejecutamos el


có digo, vemos quetemperaturatiene el valor esperado, peroXno es. Subiendo el
có digo, insertamos una declaració n de impresió n despué s de la líneatemperatura
= xy descubre que ambostemperaturayXtener el valor['a', 'b']. Una
inspecció n rá pida del có digo revela que enespalnosotros escribimostemperatura
inversaen vez detemp.reversa()—la evaluació n detemperatura inversadevuelve el
incorporadocontrarrestarmé todo para listas, pero no lo invoca.35

Ejecutamos la prueba nuevamente, y ahora parece que tanto temp como x tienen el
valor ['b', 'a']. Ahora hemos reducido el error a una línea. Parece que temp.reverse()
cambió inesperadamente el valor de x. Nos ha picado un error de alias: temp y x son
nombres para la misma lista, tanto antes como despué s de que la lista se invierta.
Una forma de solucionarlo es reemplazar la primera declaració n de asignació n en
isPal por temp = x[:], lo que hace que se haga una copia de x. La versió n corregida de
isPal es
def esAmigo(x):
"""Asume que x es una lista
Devuelve True si la lista es un palíndromo; Falso de lo
contrario""" temp = x[:]
temp.reverse()
si temp == x:
devolver
Verdadero más:
falso retorno

6.2.3 Cuando las cosas se ponen difíciles


Se dice que Joseph P. Kennedy, padre del presidente Kennedy, instruyó a sus
hijos: “Cuando las cosas se ponen difíciles, los difíciles se ponen en marcha”.36
Pero nunca depuró una pieza de software. Esta subsecció n contiene algunas
sugerencias pragmá ticas sobre qué hacer cuando la depuració n se vuelve difícil.

 Busque a los sospechosos habituales. por ejemplo, tienes


o Pasó argumentos a una funció n en el orden incorrecto,
o Escribió mal un nombre, por ejemplo, escribió una letra
minú scula cuando debería haber escrito una mayú scula,
o No se pudo reinicializar una variable,
o Probado que dos valores de punto flotante son iguales ( ==) en
lugar de casi igual (recuerde que la aritmé tica de punto flotante no
es lo mismo que la aritmética que aprendió en la escuela),
o Probó la igualdad de valores (p. ej., comparó dos listas escribiendo
la expresió nL1 == L2) cuando te referías a la igualdad de objetos
(p. ej.,identificación (L1) == identificación (L2)),
o Olvidé que alguna funció n incorporada tiene un efecto secundario,

35Uno bien podría preguntarse por qué no hay un verificador está tico que detecte el
hecho de que la línea de có digo temp.reverse no hace que se realice ningú n cá lculo ú til y,
por lo tanto, es probable que sea un error.
36Tambié nse dice que le dijo a JFK: “No compre un solo voto má s de lo necesario. Que me
condenen si voy a pagar por un derrumbe”.
Capítulo 6. Pruebas y 8

o olvidado el()que convierte una referencia a un objeto de tipo


funciónen una invocació n de funció n,

o Creó un alias no intencional, o


o Cometió cualquier otro error que sea típico para usted.
 Deje de preguntarse por qué el programa no está haciendo lo que usted
quiere. En su lugar, pregúntese por qué está haciendo lo que está
haciendo.Esa debería ser una pregunta má s fá cil de responder y
probablemente sea un buen primer paso para descubrir có mo arreglar
el programa.
 Tenga en cuenta que el error probablemente no esté donde cree que está.Si lo
fuera, probablemente lo habrías encontrado hace mucho tiempo. Una forma
prá ctica de decidir dó nde buscar es preguntar dó nde no puede estar el
error. Como dijo Sherlock Holmes: “Elimine todos los demá s factores, y el
que queda debe ser la verdad”. 37
 Trate de explicar el problema a otra persona.Todos desarrollamos puntos
ciegos. A menudo sucede que el simple hecho de intentar explicar el
problema a alguien lo llevará a ver cosas que se le han pasado por alto. Una
buena cosa para tratar de explicar es por qué el error no puede estar en
ciertos lugares.
 No creas todo lo que lees.En particular, no crea en la documentació n. Es
posible que el có digo no esté haciendo lo que sugieren los comentarios.
 Deje de depurar y comience a escribir documentación.Esto le ayudará a
abordar el problema desde una perspectiva diferente.
 Aléjate e inténtalo de nuevo mañana. Esto puede significar que el error se
corrige má s tarde que si se hubiera quedado con él, pero probablemente
pasará mucho menos tiempo buscá ndolo. Es decir, es posible intercambiar
latencia por eficiencia. (Estudiantes, ¡esta es una excelente razó n para
comenzar a trabajar en los conjuntos de problemas de programació n má s
temprano que tarde!)

6.2.4 Y cuando hayas encontrado “el” bicho


Cuando cree que ha encontrado un error en su có digo, la tentació n de comenzar
a codificar y probar una solució n es casi irresistible. Sin embargo, a menudo es
mejor reducir un poco la velocidad. Recuerde que el objetivo no es corregir un
error, sino moverse rá pida y eficientemente hacia un programa libre de errores.

Pregú ntese si este error explica todos los síntomas observados o si es solo la punta
del iceberg. Si es lo ú ltimo, puede ser mejor pensar en solucionar este error junto con
otros cambios. Supongamos, por ejemplo, que ha descubierto que el error es el
resultado de haber mutado accidentalmente una lista. Podría eludir el problema
localmente (quizá s haciendo una copia de la lista), o podría considerar usar una
tupla en lugar de una lista (ya que las tuplas son inmutables), quizá s eliminando
errores similares en otras partes del có digo.

Antes de realizar cualquier cambio, intente comprender la ramificació n de la


"solució n" propuesta. ¿Romperá algo má s? ¿Introduce excesiva complejidad?
¿Ofrece la oportunidad de ordenar otras partes del có digo?

37Arthur Conan Doyle, “El signo de los cuatro”.


8 Capítulo 6. Pruebas y

Siempre asegú rese de que puede volver a donde está . No hay nada má s frustrante
que darse cuenta de que una larga serie de cambios te han dejado má s lejos de la
meta que cuando empezaste y no tener forma de volver a donde empezaste. El
espacio en disco suele ser abundante. Ú selo para almacenar versiones antiguas de
su programa.

Finalmente, si hay muchos errores inexplicables, puede considerar si encontrar y


corregir los errores uno a la vez es el enfoque correcto. Tal vez sería mejor que
pensara si hay alguna forma mejor de organizar su programa o algú n algoritmo
má s simple que sea má s fá cil de implementar correctamente.
Capítulo 6. Pruebas y 8

7EXCEPCIONES Y AFIRMACIONES

Una "excepció n" generalmente se define como "algo que no se ajusta a la norma"
y, por lo tanto, es algo raro. No hay nada raro en las excepciones en Python.
Está n en todos lados. Prá cticamente todos los mó dulos de la biblioteca está ndar
de Python los usan, y Python mismo los generará en muchas circunstancias
diferentes. Ya has visto algunos de ellos.

Abra un shell de Python e ingrese,


prueba =
[1,2,3]
prueba[3]

y el intérprete responderá con algo como


Rastreo (llamadas recientes más última):
Archivo "<pyshell#1>", línea 1, en
<módulo> prueba[3]
IndexError: índice de lista fuera de rango
IndexError es el tipo de excepción que genera Python cuando un
programa intenta acceder a un elemento que no está dentro de los
límites de un tipo indexable. La cadena que sigue a IndexError
proporciona información adicional sobre la causa de la excepción.

La mayoría de las excepciones integradas de Python se ocupan de situaciones en


las que un programa ha intentado ejecutar una declaració n sin la semá ntica
adecuada. (Nos ocuparemos de las excepciones excepcionales, aquellas que no se
ocupan de los errores, má s adelante en este capítulo.) Aquellos lectores (todos
ustedes, esperamos) que hayan intentado escribir y ejecutar programas en Python
ya se habrá n encontrado con muchos de estos. Entre los tipos de excepciones que
ocurren con mayor frecuencia se encuentran TypeError, NameError y ValueError.

7.1 Manejo de excepciones


Hasta ahora, hemos tratado las excepciones como eventos fatales. Cuando se
genera una excepció n, el programa finaliza (bloqueos podría ser una palabra má s
apropiada en este caso), y volvemos a nuestro có digo e intentamos averiguar qué
salió mal. Cuando se genera una excepció n que hace que el programa finalice,
decimos que se ha generado una excepció n no controlada.

No es necesario que una excepció n conduzca a la finalizació n del programa. Las


excepciones, cuando se generan, pueden y deben ser manejadas por el
programa. A veces, se genera una excepció n porque hay un error en el programa
(como acceder a una variable que no existe), pero muchas veces, una excepció n
es algo que el programador puede y debe anticipar. Un programa podría
intentar abrir un archivo que no existe. Si un programa interactivo le pide a un
usuario una entrada, el usuario podría ingresar algo inapropiado.
Capítulo 7. Excepciones y 8

Si sabe que una línea de có digo puede generar una excepció n cuando se ejecuta,
debe manejar la excepció n. En un programa bien escrito, las excepciones no
controladas deberían ser la excepció n.

Considere el có digo
SuccessFailureRatio = numÉxitos/float(numFailures) print
'La proporción de éxito/fracaso es', SuccessFailureRatio
print 'Ahora aquí'

La mayoría de las veces, este có digo funcionará bien, pero fallará si numFailures
resulta ser cero. El intento de dividir por cero hará que el sistema de tiempo de
ejecució n de Python genere una excepció n ZeroDivisionError, y nunca se alcanzará n
las declaraciones de impresió n.

Hubiera sido mejor haber escrito algo en la línea de


intentar:
SuccessFailureRatio = numÉxitos/float(numFailures) print
'La relación éxito/fracaso es', SuccessFailureRatio
excepto ZeroDivisionError:
print 'No hay fallas, por lo que la proporción de éxito/falla no
está definida.' imprimir 'Ahora aquí'

Al entrar en elintentarbloquear, el inté rprete intenta evaluar la


expresió nnumÉxitos/float(numErrores). Si la evaluació n de la expresió n es
exitosa, el programa asigna el valor de la expresió n a la variableéxitoFracasoRatio,
ejecuta elimprimirdeclaració n al final de laintentarbloque, y procede a
laimprimirdeclaració n que sigue a laprobar-excepto. Si, sin embargo, unError de
división cerose genera una excepció n durante la evaluació n de la expresió n, el
control salta inmediatamente a la exceptobloquear(saltá ndose la tarea y
elimprimirdeclaració n en elintentarbloque), elimprimirdeclaració n en elexceptose
ejecuta el bloque, y luego la ejecució n continú a en elimprimirdeclaració n que sigue a
laprobar-exceptobloquear.

Ejercicio de dedos:Implemente una funció n que cumpla con las especificaciones a


continuació n. Usar unaprobar-exceptobloquear.
def sumDigits(s):
"""Asume que s es una cadena
Devuelve la suma de los dígitos decimales en s
Por ejemplo, si s es 'a2b3c', devuelve 5"""

Veamos otro ejemplo. Considere el có digo


val = int(raw_input('Ingrese un número entero: '))
print 'El cuadrado del número que ingresaste es', val**2

Si el usuario amablemente escribe una cadena que se puede convertir en un


nú mero entero, todo estará bien. Pero supongamos que el usuario escribe abc?
La ejecució n de la línea de có digo hará que el sistema de tiempo de ejecució n de
Python genere una excepció n ValueError y nunca se alcanzará la declaració n de
impresió n.
8 Capítulo 7. Excepciones y

Lo que el programador debería haber escrito sería algo así como


mientras que es cierto:
val = raw_input('Ingrese un número
entero:') intente:
valor = int(valor)
imprime 'El cuadrado del número que ingresaste es',
val**2 break #para salir del bucle while
excepto ValueError:
print val, 'no es un entero'

Despué s de ingresar al bucle, el programa le pedirá al usuario que ingrese un


nú mero entero. Una vez que el usuario ha ingresado algo, el programa ejecuta el
bloque try-except. Si ninguna de las dos primeras instrucciones en el bloque try
provoca que se genere una excepció n ValueError, se ejecuta la instrucció n break y
se sale del ciclo while. Sin embargo, si la ejecució n del có digo en el bloque de
prueba genera una excepció n ValueError, el control se transfiere inmediatamente
al có digo en el bloque de excepció n.
Por lo tanto, si el usuario ingresa una cadena que no representa un nú mero entero,
el programa le pedirá al usuario que intente nuevamente. No importa qué texto
ingrese el usuario, no causará una excepció n no controlada.

La desventaja de este cambio es que el texto del programa ha pasado de dos


líneas a ocho. Si hay muchos lugares donde se le pide al usuario que ingrese un
nú mero entero, esto puede ser problemá tico. Por supuesto, este problema se
puede resolver introduciendo una funció n:
def readInt():
while True:
val = raw_input('Ingrese un número
entero:') intente:
valor =
int(valor)
devuelve valor
excepto ValueError:
print val, 'no es un entero'

Mejor aú n, esta funció n se puede generalizar para solicitar cualquier tipo de entrada,
def readVal(valType, requestMsg, errorMsg):
while True:
val = raw_input(requestMsg + ' ')
intente:
val = valType(val)
devuelve val
excepto ValueError:
print val, errorMsg

La funció n readVal es polimó rfica, es decir, funciona para argumentos de muchos


tipos diferentes. Estas funciones son fá ciles de escribir en Python, ya que los tipos
son valores de primera clase. Ahora podemos pedir un nú mero entero usando el
có digo
val = readVal(int, 'Ingrese un entero:', 'no es un entero')

Las excepciones pueden parecer hostiles (después de todo, si no se manejan, una


excepció n hará que el programa se bloquee), pero considere la alternativa. ¿Qué
debería hacer la conversió n de tipo int, por ejemplo, cuando se le pide que convierta
la cadena 'abc' en un objeto de tipo int? Podría devolver un nú mero entero
correspondiente a los bits utilizados para codificar la cadena, pero es poco probable
que tenga alguna relació n con la intenció n del programador. Alternativamente,
podría devolver el valor especial Ninguno. Si hizo eso,
Capítulo 7. Excepciones y 8

el programador necesitaría insertar có digo para comprobar si la conversió n de tipo


había devuelto Ninguno. Un programador que olvidara esa verificació n correría el
riesgo de obtener algú n error extrañ o durante la ejecució n del programa.

Con las excepciones, el programador todavía necesita incluir el có digo que se


ocupa de la excepció n. Sin embargo, si el programador se olvida de incluir dicho
có digo y se genera la excepció n, el programa se detendrá de inmediato. É sto es
una cosa buena. Alerta al usuario del programa sobre el hecho de que ha ocurrido
algo problemá tico. (Y, como discutimos en el ú ltimo capítulo, los errores
evidentes son mucho mejores que los errores encubiertos). Ademá s, le da a
alguien que está depurando el programa una indicació n clara de dó nde salieron
mal las cosas.

Si es posible que un bloque de có digo de programa genere má s de un tipo de


excepció n, la palabra reservada excepto puede ir seguida de una tupla de
excepciones, por ejemplo,
excepto (ValueError, TypeError):

en cuyo caso se ingresará el bloque de excepció n si alguna de las excepciones


enumeradas se genera dentro del bloque de prueba. Alternativamente, podemos
escribir un bloque de excepció n separado para cada tipo de excepció n, lo que
permite que el programa elija una acció n en funció n de la excepció n que se generó .
Si el programador escribe
excepto:

se ingresará el bloque de excepció n si se genera algú n tipo de excepció n dentro del intento
bloquear. Estas características se muestran en la Figura 7.1.

7.2 Excepciones como mecanismo de flujo de control


No piense en las excepciones como puramente por errores. Son un mecanismo
conveniente de flujo de control que puede usarse para simplificar programas.

En muchos lenguajes de programació n, el enfoque está ndar para lidiar con los
errores es hacer que las funciones devuelvan un valor (a menudo algo aná logo al
Ninguno de Python) que indica que algo salió mal. Cada invocació n de funció n
tiene que comprobar si se ha devuelto ese valor. En Python, es má s comú n que
una funció n genere una excepció n cuando no puede producir un resultado que
sea consistente con la especificació n de la funció n.

La declaració n de aumento de Python obliga a que ocurra una excepció n específica.


La forma de una declaració n de aumento es
aumentarexcepciónNombre(argumentos)

El nombre de excepció n suele ser una de las excepciones integradas, por ejemplo,
ValueError. Sin embargo, los programadores pueden definir nuevas excepciones
creando una subclase (consulte el Capítulo 8) de la clase integrada Exception.
Diferentes tipos de excepciones pueden tener diferentes tipos de argumentos, pero
la mayoría de las veces el argumento es una sola cadena, que se usa para describir la
razó n por la que se genera la excepció n.
8 Capítulo 7. Excepciones y

Ejercicio de dedos:Implementar una funció n que satisfaga la especificació n.


def encontrarUnEven(l):
"""Asume que l es una lista de
números enteros Devuelve el primer
número par en l
Genera ValueError si l no contiene un número par"""

Considere la definició n de la funció n en la figura 7.1.

Figura 7.1 Uso devect2):


def obtenerRatios(vect1, excepciones para el flujo de control
"""Asume: vect1 y vect2 son listas de igual longitud de números
Hay dos bloques excepto
Devuelve: unaasociados
lista que concontiene
el bloquelos
try. Si se genera
valores una excepció nde
significativos
dentro del bloquevect1[i]/
de prueba, Python primero verifica si se trata de un
vect2[i]"""Siproporciones
ZeroDivisionError. = valor especial, nan, de tipo float a ratios. (El
es así, añ ade un
[]
valor nan significa “no es un nú mero”. No tiene un literal, pero se puede denotar
para el índice en el rango
convirtiendo la cadena
(len 'nan' ointente:
(vect1)): la cadena 'NaN' en tipo float. Cuando nan se usa
como operando en un expresió n de tipo float, el valor de esa expresió n tambié n es
ratios.append(vect1[índice]/float(vect2[índice]))
excepton es
nan). Si la excepció ZeroDivisionError:
distinta de ZeroDivisionError, el có digo ejecuta el segundo
ratios.append(float('nan')) #nan = No es un número
bloque excepto, que
excepto: genera una excepció n ValueError con una cadena asociada.
aumentar ValueError('getRatios llamado con malos
En principio, nunca se debe ingresar el segundo bloque excepto, porque el có digo
que invoca getRatios debe respetar las suposiciones en la especificació n de
getRatios. Sin embargo, dado que verificar estas suposiciones impone solo una carga
computacional insignificante, probablemente valga la pena practicar la
programació n defensiva y verificar de todos modos.

El siguiente có digo ilustra có mo un programa podría usarobtenerRatios. El


nombremensajeEn la lineaexcepto ValueError, mensaje:está vinculado al
argumento (una cadena en este caso) asociado con ValorErrorcuando fue levantado.
cuando se ejecuta
Capítulo 7. Excepciones y 8

intentar:
imprimir getRatios([1.0,2.0,7.0,6.0], [1.0,2.0,0.0,3.0])
imprimir obtenerRatios([], [])
imprime getRatios([1.0, 2.0], [3.0])
excepto ValueError, mensaje:
imprimir mensaje

huellas dactilares
[1.0, 1.0, nan, 2.0]
[]
getRatios llamado con malos argumentos

La figura 7.2 contiene una implementació n de la misma especificació n, pero sin


utilizar una excepció n de prueba.

Figura 7.2 Flujo de control sin prueba excepto


def obtenerRatios(vect1, vect2):
"""Asume: vect1 y vect2 son listas de igual longitud de números
El có digo de la Figura 7.2 es má s largo y má s difícil de leer que el có digo de la
Devuelve: una lista que contiene los valores significativos
Figura 7.1.de
También es menos eficiente. (El có digo de la Figura 7.2 se podría
acortar ligeramente eliminando las variables locales vect1Elem y vect2Elem, pero
vect1[i]/
vect2[i]""" proporciones
solo a costa de introducir =
aú n má s ineficiencia al acceder a cada elemento
[]
repetidamente).
si len(vect1) != len(vect2):
aumentar ValueError ('getRatios llamado con argumentos
Veamos un ejemplo má s.
incorrectos') para el índice en el rango (len (vect1)):
vect1Elem =
vect1[índice]
vect2Elem =
vect2[índice]
si (tipo (vect1Elem) no en (int, float))\
o (escriba (vect2Elem) no en (int, float)):
aumentar ValueError('getRatios llamado con malos
argumentos') if vect2Elem == 0.0:
9 Capítulo 7. Excepciones y

def
getGrades(fname
): prueba:
gradesFile = open(fname, 'r') #abrir archivo para
lectura excepto IOError:
aumentar ValueError('getGrades no pudo abrir' + fname)
calificaciones = []
para la línea en
gradesFile:
intente:
grades.append(float(line))
excepto:
aumentar ValueError('No se puede convertir la línea
en flotante') devolver calificaciones

intentar:
calificaciones =
getGrades('cuestionario1calificaciones.
txt') calificaciones.sort()
mediana =
grados[len(grados)//2] print
Figura 7.3 Obtener calificaciones

La funció n getGrades devuelve un valor o genera una excepció n con la que tiene
asociado un valor. Genera una excepció n ValueError si la llamada para abrir genera
un IOError. Podría haber ignorado el IOError y dejar que la parte del programa que
llama a getGrades se encargue de ello, pero eso habría proporcionado menos
informació n al có digo de llamada sobre lo que salió mal. El có digo que usa
getGrades usa el valor devuelto para calcular otro valor o maneja la excepció n e
imprime un mensaje de error ú til.

7.3 afirmaciones
La declaració n de afirmació n de Python proporciona a los programadores una forma
sencilla de confirmar que el estado del cálculo es el esperado. Una declaració n de
afirmació n puede tomar una de dos formas:
afirmarexpresión booleana

o
afirmarexpresión booleana,argumento

Cuando se encuentra una declaració n de afirmació n, se evalú a la expresió n


booleana. Si se evalú a como Verdadero, la ejecució n continú a a su manera. Si se
evalú a como False, se genera una excepció n AssertionError.

Las aserciones son una herramienta ú til de programació n defensiva. Se pueden usar
para confirmar que los argumentos de una funció n son de tipos apropiados. Tambié n
son una ú til herramienta de depuració n. Se puede usar, por ejemplo, para confirmar
que los valores intermedios tienen los valores esperados o que una funció n devuelve
un valor aceptable.
8CLASES Y PROGRAMACIÓN ORIENTADA A OBJETOS

Ahora dirigimos nuestra atenció n a nuestro ú ltimo tema principal relacionado con
la escritura de programas en Python: el uso de clases para organizar programas en
torno a mó dulos y abstracciones de datos.

Las clases se pueden utilizar de muchas maneras diferentes. En este libro


enfatizamos su uso en el contexto de la programació n orientada a objetos. La clave
de la programació n orientada a objetos es pensar en los objetos como colecciones
de datos y métodos que operan con esos datos.

Las ideas que subyacen a la programació n orientada a objetos tienen unos cuarenta
añ os y han sido ampliamente aceptadas y practicadas durante los ú ltimos veinte
añ os má s o menos. A mediados de la década de 1970, la gente comenzó a escribir
artículos que explicaban los beneficios de este enfoque de la programació n. Casi al
mismo tiempo, los lenguajes de programació n SmallTalk (en Xerox PARC) y CLU (en
MIT) brindaron apoyo lingü ístico a las ideas. Pero no fue hasta la llegada de C++ y
Java que realmente despegó en la prá ctica.

Hemos confiado implícitamente en la programació n orientada a objetos a lo largo de


la mayor parte de este libro. En la Secció n 2.1.1 dijimos “Los objetos son las cosas
centrales que manipulan los programas de Python. Cada objeto tiene un tipo que
define el tipo de cosas que los programas pueden hacer con objetos de ese tipo”.
Desde el Capítulo 5, nos hemos basado en gran medida en los tipos integrados, como
list y dict, y en los métodos asociados con esos tipos. Pero así como los diseñ adores
de un lenguaje de programació n pueden construir solo una pequeñ a fracció n de las
funciones ú tiles, solo pueden construir una pequeñ a fracció n de los tipos ú tiles. Ya
hemos visto un mecanismo que permite a los programadores definir nuevas
funciones; ahora vemos un mecanismo que permite a los programadores definir
nuevos tipos.

8.1 Clases y tipos de datos abstractos


La noció n de un tipo de datos abstracto es bastante simple. Un tipo de datos
abstracto es un conjunto de objetos y las operaciones sobre esos objetos. Estos
está n unidos para que uno pueda pasar un objeto de una parte de un programa a
otra y, al hacerlo, proporciona acceso no solo a los atributos de datos del objeto,
sino tambié n a operaciones que facilitan la manipulació n de esos datos.

Las especificaciones de esas operaciones definen una interfaz entre el tipo de datos
abstracto y el resto del programa. La interfaz define el comportamiento de las
operaciones: lo que hacen, pero no có mo lo hacen. Por tanto, la interfaz
proporciona una barrera de abstracció n que aísla el resto del programa de las
estructuras de datos, algoritmos y có digo involucrados en proporcionar una
realizació n de la abstracció n de tipo.

La programació n se trata de gestionar la complejidad de una manera que facilite


el cambio. Hay dos poderosos mecanismos disponibles para lograr esto:
descomposició n y abstracció n. La descomposició n crea estructura en un
programa y la abstracció n suprime los detalles. La clave es suprimir el
apropiado
9 Capítulo 8. Clases y programación orientada a

detalles. Aquí es donde la abstracció n de datos da en el blanco. Se pueden crear


tipos específicos de dominio que proporcionen una abstracció n conveniente.
Idealmente, estos tipos capturan conceptos que será n relevantes durante la vida
ú til de un programa. Si uno comienza el proceso de programació n ideando tipos
que será n relevantes meses e incluso dé cadas despué s, tiene una gran ventaja en
el mantenimiento de ese software.

Hemos estado usando tipos de datos abstractos (sin llamarlos así) a lo largo de
este libro. Hemos escrito programas utilizando nú meros enteros, listas, nú meros
de coma flotante, cadenas y diccionarios sin pensar en có mo se podrían
implementar estos tipos. Parafraseando a Bourgeois Gentilhomme de Moliè re,
“Par ma foi, il ya plus de quatre-vingt pages que nous avons utilisé ADTs, sans
que nous le sachions”.38

En Python, uno implementa abstracciones de datos usando clases. La figura 8.1


contiene una definició n de clase que proporciona una implementació n sencilla de
una abstracció n de conjunto de enteros llamada IntSet.

Una definició n de clase crea un objeto de tipo tipo y asocia con ese objeto un
conjunto de objetos de tipo mé todo de instancia. Por ejemplo, la expresió n
IntSet.insert hace referencia al mé todo insert definido en la definició n de la clase
IntSet. y el codigo
tipo de impresión (IntSet), tipo (IntSet.insertar)

imprimirá

<tipo 'tipo'> <tipo 'método de instancia'>

Tenga en cuenta que la cadena de documentació n (el comentario encerrado en


""") en la parte superior de la definició n de la clase describe la abstracció n
proporcionada por la clase, no informació n sobre có mo se implementa la clase. El
comentario debajo de la cadena de documentació n contiene informació n sobre la
implementació n. La informació n está dirigida a los programadores que deseen
modificar la implementació n o crear subclases (consulte la Secció n 8.2) de la
clase, no a los programadores que deseen utilizar la abstracció n.

38“Dios mío, durante má s de ochenta pá ginas hemos estado usando ADT sin saberlo”.
Capítulo 8. Clases y programación orientada a 9

clase IntSet(objeto):
"""Un intSet es un conjunto de enteros"""
#Información sobre la implementación (no la abstracción)
#El valor del conjunto está representado por una lista de enteros,
self.vals. #Cada int en el conjunto ocurre en self.vals exactamente
una vez.

def __init__(uno mismo):


"""Crear un conjunto vacío de
enteros""" self.vals = []

def insert(self, e):


"""Asume que e es un número entero e inserta e en
self""" si no es e en self.vals:
self.vals.append(e)

def miembro(self, e): """Asume


que e es un número entero
Devuelve True si e está en self, y False en caso
contrario""" return e in self.vals

def remove(self, e):


"""Asume que e es un número entero y elimina e de sí
mismo Genera ValueError si e no está en sí
mismo"""
intentar:
self.vals.remove(e)
excepto:
aumentar ValueError(str(e) + 'no encontrado')

def getMembers(self):
"""Devuelve una lista que contiene los elementos de self.
No se puede suponer nada sobre el orden de los elementos"""
return self.vals[:]

def __str__(uno mismo):


"""Devuelve una representación de cadena de uno mismo"""
self.vals.sort()
resultado = ''
para e en self.vals:
resultado = resultado + str(e) + ','
return '{' + result[:-1] + '}' #-1 omite la coma final

Figura 8.1 ClaseIntSet

Cuando se produce una definició n de funció n dentro de una definició n de clase,


la funció n definida se denomina mé todo y se asocia con la clase. Estos mé todos a
veces se denominan atributos de mé todo de la clase. Si esto le parece confuso en
este momento, no se preocupe. Tendremos mucho má s que decir sobre este
tema má s adelante en este capítulo.

Las clases admiten dos tipos de operaciones:

 instanciaciónse utiliza para crear instancias de la clase. Por ejemplo, la


declaració ns = ConjuntoInt()crea un nuevo objeto de tipo IntSet. Este
objeto se llama una instancia deIntSet.
 Referencias de atributosutilice la notació n de puntos para acceder a los
atributos asociados con la clase. Por ejemplo,s.miembrose refiere al
métodomiembroasociado con la instanciasde tipoIntSet.
9 Capítulo 8. Clases y programación orientada a

Cada definició n de clase comienza con la palabra reservada class seguida del nombre de
la clase y alguna informació n sobre có mo se relaciona con otras clases. En este caso, la
primera línea indica que IntSet es una subclase de objeto. Por ahora, ignore lo que
significa ser una subclase. Llegaremos a eso en breve.

Como veremos, Python tiene varios nombres de mé todos especiales que


comienzan y terminan con dos guiones bajos. El primero de estos que veremos es
init . Cada vez que se crea una instancia de una clase, se realiza una llamada al
método init definido en esa clase. Cuando la línea de có digo
s = ConjuntoInt()

se ejecuta, el intérprete creará una nueva instancia de tipo IntSet y luego llamará a
IntSet. init con el objeto recién creado como el pará metro real que está vinculado al
pará metro formal self. Cuando se invoca, IntSet. init crea vals, un objeto de tipo list, que
pasa a formar parte de la instancia recié n creada de tipo IntSet. (La lista se crea usando
la ya conocida notació n [], que es simplemente una abreviatura de list()). Esta lista se
denomina atributo de datos de la instancia de IntSet. Tenga en cuenta que cada objeto
de tipo IntSet tendrá una lista de valores diferente, como cabría esperar.

Como hemos visto, los métodos asociados con una instancia de una clase se
pueden invocar usando la notació n de puntos. Por ejemplo, el có digo,
s = IntSet()
s.insertar(3
)
imprimir en miembro(3)

crea una nueva instancia de IntSet, inserta el nú mero entero 3 en ese IntSet y
luego imprime True.

A primera vista, parece haber algo inconsistente aquí. Parece como si cada
método se llamara con un argumento demasiado pequeñ o. Por ejemplo, el
miembro tiene dos pará metros formales, pero parece que lo estamos llamando
con un solo pará metro real. Este es un artefacto de la notació n de puntos. El
objeto asociado con la expresió n que precede al punto se pasa implícitamente
como el primer pará metro del mé todo. A lo largo de este libro, seguimos la
convenció n de usar self como el nombre del pará metro formal al que está
ligado este pará metro real.
Los programadores de Python observan esta convenció n casi universalmente,
y le sugerimos encarecidamente que también la use.

Una clase no debe confundirse con instancias de esa clase, al igual que un objeto de
tipo lista no debe confundirse con el tipo de lista. Los atributos se pueden asociar
con una clase en sí o con instancias de una clase:

 Los atributos de mé todo se definen en una definició n de clase, por


ejemploIntSet.miembroes un atributo de la claseIntSet. Cuando la clase es
instanciada, por ejemplo, pors = ConjuntoInt(), atributos de instancia, por
ejemplo,s.miembro, son creados. Manten eso en
menteIntSet.miembroys.miembroson objetos diferentes.
Mientrass.miembroestá inicialmente ligado a lamiembrométodo definido en la
claseIntSet, ese enlace se puede cambiar durante el curso de un cá lculo. Por
ejemplo, podrías (¡pero no deberías!) escribir
s.miembro = IntSet.insert.
Capítulo 8. Clases y programación orientada a 9

 Cuando los atributos de datos está n asociados con una clase, los llamamos
variables de clase. Cuando está n asociadas con una instancia, las llamamos
variables de instancia. Por ejemplo,valses una variable de instancia porque
para cada instancia de claseIntSet,valsestá vinculado a una lista diferente.
Hasta ahora, no hemos visto una variable de clase. Usaremos uno en la
Figura 8.3.
La abstracció n de datos logra la independencia de la representació n. Piense en la
implementació n de un tipo abstracto como si tuviera varios componentes:

 Implementaciones de los mé todos del tipo,


 Estructuras de datos que juntas codifican valores del tipo, y
 Convenciones sobre có mo las implementaciones de los métodos son para
usar las estructuras de datos. Una convenció n clave es capturada por la
representació n invariante.
La representació n invariable define qué valores de los atributos de datos
corresponden a representaciones vá lidas de instancias de clase. La representació n
invariable para IntSet es que vals no contiene duplicados. La implementació n de
init es responsable de establecer el invariante (que se mantiene
en la lista vacía), y los otros métodos son responsables de
mantener ese invariante. Es por eso que insert appends e solo si
aún no está en self.vals.

La implementació n de remove explota la suposició n de que la representació n


invariante se cumple cuando se ingresa remove. Llama a list.remove solo una vez,
ya que la representació n invariante garantiza que haya como má ximo una
aparició n de e en self.vals.

El ú ltimo mé todo definido en la clase,str , es otro de esos métodos


especiales. Cuando se utiliza el comando de impresión, se invoca
automáticamente la función str asociada con el objeto que se va a imprimir.
Por ejemplo, el código

s = IntSet()
s.insertar(3
)
s.insertar(4
) imprimir s

imprimirá ,
{3,4}

(Sisin callese definió el método,imprimircausaría algo como


<objeto principal .IntSet en 0x1663510>para ser impreso.) También
podríamos imprimir el valor desescribiendoimprimir s. cadena ()o inclusoimprimir
IntSet. calle (s), pero
usar esos formularios es menos conveniente. El método str de una clase tambié n es
se invoca cuando un programa convierte una instancia de esa clase en una cadena llamando
calle.
9 Capítulo 8. Clases y programación orientada a

8.1.1 Diseño de programas utilizando tipos de datos abstractos


Los tipos de datos abstractos son un gran problema. Conducen a una manera
diferente de pensar acerca de la organizació n de grandes programas. Cuando
pensamos en el mundo, nos basamos en abstracciones. En el mundo de las finanzas
la gente habla de acciones y bonos. En el mundo de la biología se habla de proteínas
y residuos. Cuando tratamos de comprender estos conceptos, reunimos
mentalmente algunos de los datos y características relevantes de este tipo de
objetos en un paquete intelectual. Por ejemplo, pensamos que los bonos tienen una
tasa de interés y una fecha de vencimiento como atributos de datos. También
pensamos que los bonos tienen operaciones como "establecer precio" y "calcular el
rendimiento hasta el vencimiento". Los tipos de datos abstractos nos permiten
incorporar este tipo de organizació n en el diseñ o de programas.

La abstracció n de datos alienta a los diseñ adores de programas a centrarse en la


centralidad de los objetos de datos en lugar de las funciones. Pensar en un
programa má s como una colecció n de tipos que como una colecció n de funciones
conduce a un principio organizativo profundamente diferente. Entre otras cosas,
anima a pensar en la programació n como un proceso de combinació n de
fragmentos relativamente grandes, ya que las abstracciones de datos suelen
abarcar má s funciones que las funciones individuales. Esto, a su vez, nos lleva a
pensar en la esencia de la programació n como un proceso no de escribir líneas de
có digo individuales, sino de componer abstracciones.

La disponibilidad de abstracciones reutilizables no solo reduce el tiempo de


desarrollo, sino que tambié n suele conducir a programas má s fiables, porque el
software maduro suele ser má s fiable que el software nuevo. Durante muchos añ os,
las ú nicas bibliotecas de programas de uso comú n eran estadísticas o científicas.
Hoy, sin embargo, existe una gran variedad de bibliotecas de programas disponibles
(especialmente para Python), a menudo basadas en un rico conjunto de
abstracciones de datos, como veremos má s adelante en este libro.

8.1.2 Uso de clases para realizar un seguimiento de los estudiantes y la


facultad
Como ejemplo de uso de las clases, imagine que está diseñ ando un programa
para ayudar a realizar un seguimiento de todos los estudiantes y profesores de
una universidad. Ciertamente, es posible escribir un programa de este tipo sin
utilizar la abstracció n de datos. Cada estudiante tendría un apellido, un nombre
de pila, una direcció n de casa, un añ o, algunas calificaciones, etc.
Todo esto podría guardarse en alguna combinació n de listas y diccionarios.
Hacer un seguimiento de la facultad y el personal requeriría algunas estructuras
de datos similares y algunas estructuras de datos diferentes, por ejemplo,
estructuras de datos para realizar un seguimiento de cosas como el historial de
salarios.

Antes de apresurarnos a diseñ ar un montó n de estructuras de datos, pensemos


en algunas abstracciones que podrían resultar ú tiles. ¿Existe una abstracció n
que cubra los atributos comunes de estudiantes, profesores y personal? Algunos
dirían que todos son humanos. La figura 8.2 contiene una clase que incorpora
algunos de los atributos comunes (nombre y fecha de nacimiento) de los
humanos. Hace uso del mó dulo datetime de la biblioteca está ndar de Python,
que proporciona muchos mé todos convenientes para crear y manipular fechas.
Capítulo 8. Clases y programación orientada a 9

importar fecha y hora

clase Persona(objeto):

def __init__(self,
name): """Crear una
persona"""
self.name = name
try:
lastBlank = nombre.rindex(' ')
self.lastName =
nombre[lastBlank+1:]
excepto:
self.lastName =
nombre self.birthday =
Ninguno

def getNombre(self):
"""Devuelve el nombre
completo de uno mismo"""
return self.name

def getLastName(self):
"""Devuelve el apellido
propio""" return
self.lastName

def setbirthday(self, fecha de nacimiento):


"""Asume que la fecha de nacimiento es del
tipo datetime.date Establece el cumpleaños
de uno mismo como fecha de nacimiento"""
self.cumpleaños = fecha de nacimiento

def obtenerEdad(self):
"""Devuelve la edad actual de uno
mismo en días""" if self.birthday ==
Ninguno:
aumentar ValueError
volver (fechahora.fecha.hoy() - yo.cumpleaños).días

def __lt__(uno mismo, otro):


"""Devuelve Verdadero si el nombre propio es
lexicográficamente menor que el nombre del otro, y
Falso en caso contrario"""
Figura 8.2 ClasePersona

El siguiente có digo hace uso de Person.


yo = Persona('Michael Guttag')
él = Persona('Barack Hussein Obama')
ella = Persona('Madonna')
imprímalo.getLastName()
él.establezcaCumpleaños(fechahora.fecha(1961, 8,
4))
ella.establecerCumpleaños(fechahora.fecha(1958, 8, 16))
imprima him.getName(), 'is', him.getAge(), 'days old'

Tenga en cuenta que cada vez que se crea una instancia de Persona, se proporciona un
argumento a la
función de inicio. En general, cuando creamos una instancia de una
clase, necesitamos mirar el
9 Capítulo 8. Clases y programación orientada a

especificació n de la funció n init para esa clase para saber qué argumentos
proporcionar y qué propiedades deberían tener esos argumentos.

Despué s de ejecutar este có digo, habrá tres instancias de la clase Person. Luego se
puede acceder a la informació n sobre estas instancias utilizando los mé todos
asociados con ellas. Por ejemplo, him.getLastName() devolverá 'Obama'. La
expresió n him.lastName también devolverá 'Obama'; sin embargo, por razones que
se analizan má s adelante en este capítulo, escribir expresiones que acceden
directamente a variables de instancia se considera de mala forma y debe evitarse.
De manera similar, no existe una forma apropiada para que un usuario de la
abstracció n Person extraiga el cumpleañ os de una persona, a pesar de que la
implementació n contiene un atributo con ese valor. Sin embargo, existe una forma
de extraer informació n que depende del cumpleañ os de la persona, como se ilustra
en la ú ltima instrucció n impresa del có digo anterior.

ClasePersonadefine otro mé todo especialmente nombrado,lt.Este mé todo


sobrecarga el<operador. El métodopersonase llama cada vez que el primer argumento
de la<el operador es de tipoPersona. ElesEl mé todo en la clase Persona se
implementa usando el<operador de tipocalle. La expresionself.Nombre <
otro.nombrees una abreviatura depropio.nombre. lt (yo.otro).
Desdepropio.nombrees de tipocalle, elesel método es el asociado con el tipocalle.

Ademá s de brindar la conveniencia sintá ctica de escribir expresiones infijas que


usan <, esta sobrecarga proporciona acceso automá tico a cualquier mé todo
polimó rfico definido usandolt. El método integrado de clasificación es uno
de esos métodos. Entonces, por ejemplo, si pList es una lista compuesta
por elementos de tipo Person, la llamada pList.sort() ordenará esa lista
usando el método lt definido en la clase Person. El código

pLista = [yo, él,


ella] para p en
pLista:
imprime p
pList.sort()
para p en
pList:
imprimir p

primero imprimirá

Michael Guttag
Barack Hussein Obama
Madonna

y luego imprimir
Michael
GuttagMadonna
Barack Hussein Obama
Capítulo 8. Clases y programación orientada a 9

8.2 Herencia
Muchos tipos tienen propiedades en comú n con otros tipos. Por ejemplo, los tipos
list y str tienen funciones len que significan lo mismo. La herencia proporciona un
mecanismo conveniente para construir grupos de abstracciones relacionadas.
Permite a los programadores crear una jerarquía de tipos en la que cada tipo hereda
atributos de los tipos que está n por encima de é l en la jerarquía.

El objeto de clase está en la parte superior de la jerarquía. Esto tiene sentido, ya que
en Python todo lo que existe en tiempo de ejecució n es un objeto. Debido a que
Person hereda todas las propiedades de los objetos, los programas pueden vincular
una variable a Person, agregar una Person a una lista, etc.

La clase MITPerson de la figura 8.3 hereda atributos de su clase principal, Person,


incluidos todos los atributos que Person heredó de su clase principal, object.

Figura 8.3 ClaseMITPersona

En la jerga declase
la programació n orientada a objetos,MITPersonaes una subclase
MITPersona(Persona):
dePersona, y por lo tanto hereda los atributos de su superclase. Ademá s de lo que
nextIdNum = 0 #número de
hereda, la subclase puede:

 identificación
Agregar nuevos def ejemplo,
atributos. Por __init__(self,
MITPersonaha agregado la
variable de clasesiguienteIdNum, la variable de instanciaidNúm, y
nombre):
el mé todogetIdNum.
Persona.__init__(self, nombre)
 self.idNum
Anularatributos =
de la superclase. Por ejemplo, MITPersonaha anuladoen
esoylt. MITPerson.nextIdNum
MITPerson.nextIdNum += 1
El mé todoMITPerson. en esoprimero invocaPersona. en esopara inicializar la
def getIdNum(self):
variable de instancia heredada
return propio.nombre. Luego se inicializaself.idNum, una
variable de instancia que instancias de MITPersonatener pero instancias
self.idNum
dePersonano.

La variable de instanciaself.idNumse inicializa usando una variable de


clase,siguienteIdNum, que pertenece a la claseMITPersona, en lugar de instancias de
la clase. Cuando una instancia de MITPersonase crea, una nueva instancia
desiguienteIdNumno se crea. Esto permiteen esopara asegurar que cada instancia
deMITPersonatiene un ú nicoidNúm.
1 Capítulo 8. Clases y programación orientada a

Considere el có digo
p1 = MITPersona('Bárbara Castor')
print str(p1) + 'El número de identificación de \'s es ' + str(p1.getIdNum())

La primera línea crea un nuevo MITPerson. La segunda línea es un poco má s


complicada. Cuando intenta evaluar la expresió n str(p1), el sistema de tiempo de
ejecució n primero verifica si hay un mé todo str asociado con la clase MITPerson.
Como no lo hay, luego verifica si hay un método str asociado con la superclase,
Person, de MITPerson. Hay, entonces usa eso.
Cuando el sistema de tiempo de ejecució n intenta evaluar la expresió n
p1.getidNum(), primero verifica si hay un método getIdNum asociado con la clase
MITPerson. Lo hay, por lo que invoca ese método e imprime
El número de identificación de Barbara Beaver es 0

(Recuerde que en una cadena, el cará cter “\” es un cará cter de escape que se usa
para indicar que el siguiente cará cter debe tratarse de manera especial. En la
cadena
'El número de identificación de \'s es '

el “\” indica que el apó strofe es parte de la cadena, no un delimitador que termina la
cadena).

Ahora considere el có digo


p1 = MITPerson('Mark Guttag')
p2 = MITPerson('Billy Bob Beaver')
p3 = MITPerson('Billy Bob Beaver')
p4 = Persona('Billy Bob Beaver')

Hemos creado cuatro personas virtuales, tres de las cuales se llaman Billy Bob Beaver.
Dos de los Billy Bobs son del tipo MITPerson y uno es simplemente una Persona. Si
ejecutamos las líneas de có digo
imprime 'p1 < p2 =', p1 <
p2 imprime 'p3 < p2 =', p3
< p2 imprime 'p4 < p1 =',
p4 < p1

el intérprete imprimirá
p1 < p2 =
Verdadero p3 <
p2 = Falso p4 <
p1 = Verdadero

Dado que p1, p2 y p3 son todos del tipo MITPerson, el intérprete utilizará el mé todo lt
definido en la clase MITPerson al evaluar las dos primeras comparaciones, por lo que el
orden se basará en nú meros de identificació n. En la tercera comparació n, el operador <
se aplica a operandos de diferentes tipos. Dado que el primer argumento de la expresió n
se usa para determinar qué método lt invocar, p4 < p1 es una abreviatura de p4. (p1).
Por tanto, el inté rprete utiliza el mé todo lt asociado al tipo de p4, Person, y las
“personas” se ordenará n por nombre.

¿Qué pasa si intentamos


imprime 'p1 < p4 =', p1 < p4
Capítulo 8. Clases y programación orientada a 1

El inté rprete invocará el operador lt asociado al tipo de p1, es decir, el definido


en la clase MITPerson. Esto conducirá a la excepció n.
AttributeError: el objeto 'Persona' no tiene atributo 'idNum'

porque el objeto al que está vinculado p4 no tiene un atributo idNum.

8.2.1 Múltiples niveles de herencia


La figura 8.4 agrega otro par de niveles de herencia a la jerarquía de clases.

estudiante de clase
(MITPerson): pase

clase UG(Estudiante):
def __init__(self, nombre, classYear):
MITPerson.__init__(self, nombre)
self.year = classYear
def getClass(self):
return self.año

clase Grad
(Estudiante):
pase
Figura 8.4 Dos clases de estudiantes

Agregar UG parece ló gico, porque queremos asociar un añ o de graduació n (o tal vez


una graduació n anticipada) con cada estudiante. Pero, ¿qué está pasando con las
clases Student y Grad? Al usar la palabra reservada de Python como cuerpo,
indicamos que la clase no tiene má s atributos que los heredados de su superclase.
¿Por qué alguien querría crear una clase sin atributos nuevos?

Al presentar la clase Grad, ganamos la capacidad de crear dos tipos diferentes de


estudiantes y usar sus tipos para distinguir un tipo de objeto de otro. Por
ejemplo, el có digo
p5 = Graduado('Buzz Aldrin')
p6 = UG('Billy Beaver', 1984)
print p5, 'es un estudiante de posgrado es', type(p5) ==
Grad print p5, 'es un estudiante de pregrado es', type(p5)
== UG

imprimirá
Buzz Aldrin es un estudiante de posgrado es cierto
Buzz Aldrin es un estudiante de pregrado es Falso

La utilidad del estudiante de tipo intermedio es un poco má s sutil. Considere volver


a la clase MITPerson y agregar el método
def esEstudiante(uno mismo):
volver isinstance(self, Student)

La funció n isinstance está integrada en Python. El primer argumento de isinstance


puede ser cualquier objeto, pero el segundo argumento debe ser un objeto de tipo type.
La funció n devuelve True si y solo si el primer argumento es una instancia del segundo
argumento. Por ejemplo, el valor de isinstance([1,2], list) es True.
1 Capítulo 8. Clases y programación orientada a

Volviendo a nuestro ejemplo, el có digo

print p5, 'es un estudiante es',


p5.isStudent() print p6, 'es un estudiante
es', p6.isStudent() print p3, 'es un
estudiante es', p3.isStudent()

huellas dactilares
Buzz Aldrin es estudiante es Verdadero
Billy Beaver es estudiante es
Verdadero Billy Bob Beaver es
estudiante es Falso

Darse cuenta deesinstancia(p6, Estudiante)es bastante diferente de


tipo(p6) == Estudiante. El objeto al quep6está encuadernado es de
tipoUG,noalumno, pero desdeUGes una subclase deAlumno, el objeto al quep6está
enlazado se considera una instancia de claseAlumno(así como una instancia
deMITPersonayPersona).

Como solo hay dos tipos de estudiantes, podríamos haber implementado


esEstudiantecomo,

def esEstudiante(uno mismo):


return type(self) == Grad or type(self) == UG

Sin embargo, si se introdujera un nuevo tipo de estudiante en algú n momento


posterior, sería necesario volver atrá s y editar el có digo que implementa
isStudent. Introduciendo la clase intermedia Student y usando isinstance evitamos
este problema. Por ejemplo, si tuvié ramos que añ adir
clase TransferenciaEstudiante(Estudiante):

def init (yo, nombre, fromSchool):


MITPerson. init (self, nombre)
self.fromSchool = fromSchool

def getOldSchool(self):
return self.fromSchool

no es necesario hacer ningú n cambio enesEstudiante.

No es inusual durante la creació n y posterior mantenimiento de un programa


volver atrá s y agregar nuevas clases o nuevos atributos a las clases antiguas. Los
buenos programadores diseñ an sus programas para minimizar la cantidad de
có digo que puede ser necesario cambiar cuando se hace eso.

8.2.2 El principio de sustitución


Cuando se utilizan subclases para definir una jerarquía de tipos, las subclases
deben considerarse como una extensió n del comportamiento de sus superclases.
Hacemos esto agregando nuevos atributos o anulando los atributos heredados de
una superclase. Por ejemplo, TransferStudent amplía Student al presentar una
escuela anterior.

A veces, la subclase anula los mé todos de la superclase, pero esto debe hacerse
con cuidado. En particular, los comportamientos importantes del supertipo
deben ser respaldados por cada uno de sus subtipos. Si el có digo del cliente
funciona correctamente utilizando una instancia del supertipo, tambié n debería
funcionar correctamente cuando se sustituye una instancia del subtipo por la
instancia del supertipo. Por ejemplo, debería
Capítulo 8. Clases y programación orientada a 1

ser posible escribir có digo de cliente usando la especificació n deAlumnoy hacer que
funcione correctamente en unEstudiante de intercambio.39

Por el contrario, no hay razó n para esperar que el có digo escrito funcione para
Estudiante de intercambiodebería funcionar para tipos arbitrarios deAlumno.

8.3 Encapsulación y Ocultación de Información


Mientras estemos tratando con estudiantes, sería una pena no hacerlos sufrir
tomando clases y sacando notas.

Grados de clase (objeto):


"""Un mapeo de estudiantes a una lista de
calificaciones""" def __init__(self):
"""Crear libro de
calificaciones vacío"""
self.students = []
self.grades = {}
self.isSorted = True

def addStudent(self, estudiante):


"""Asume: estudiante es de tipo
Estudiante
Agregar estudiante al libro de
calificaciones""" si el estudiante
está en self.students:
aumentar ValueError('Estudiante
duplicado') self.students.append(student)
self.grades[student.getIdNum()] = []
self.isSorted = False

def addGrade(self, student, grade):


"""Asume: grade es un flotante
Agregue la calificación a la lista de
calificaciones para el estudiante""" intente:
self.grades[student.getIdNum()].append(grade)
excepto:
aumentar ValueError('Estudiante no en el mapeo')

def getGrades(self, estudiante):


"""Devolver una lista de calificaciones
para el estudiante""" intente: #return
copia de las calificaciones del estudiante
devolver self.grades[student.getIdNum()][:]
excepto:
aumentar ValueError('Estudiante no en el mapeo')

def obtenerEstudiantes(self):
"""Devolver una lista de los estudiantes en el libro de
calificaciones""" si no es self.isSorted:
self.students.sort()
self.isSorted = True
return self.students[:] #return copia de la lista de estudiantes

Figura 8.5 ClaseLos grados

39Esteprincipio de sustitució n fue enunciado claramente por primera vez por Barbara
Liskov y Jeannette Wing en su artículo de 1994, "Una noció n conductual de la
subtipificació n".
1 Capítulo 8. Clases y programación orientada a

La figura 8.5 contiene una clase que se puede utilizar para realizar un
seguimiento de las calificaciones de un conjunto de estudiantes. Las instancias de
la clase Calificaciones se implementan mediante una lista y un diccionario. La
lista realiza un seguimiento de los estudiantes de la clase. El diccionario asigna el
nú mero de identificació n de un estudiante a una lista de calificaciones.

Observe que getGrades devuelve una copia de la lista de calificaciones asociadas con
un estudiante y getStudents devuelve una copia de la lista de estudiantes. El costo
computacional de copiar las listas podría haberse evitado simplemente devolviendo
las variables de instancia. Sin embargo, es probable que esto genere problemas.
Considere el có digo
todosEstudiantes =
curso1.getEstudiantes()allStudents.extend(curso2.ge
tStudents())

SiobtenerEstudiantesdevueltoauto.estudiantes, la segunda línea de có digo


tendría el efecto secundario (probablemente inesperado) de cambiar el conjunto de
estudiantes encurso1.

La variable de instancia isSorted se usa para realizar un seguimiento de si la lista de


estudiantes se ha ordenado o no desde la ú ltima vez que se agregó un estudiante. Esto
permite la implementació n de getStudents para evitar ordenar una lista ya ordenada.

La figura 8.6 contiene una funció n que utiliza Class Grades para producir un
informe de calificaciones para algunos estudiantes que toman 6.00, el curso del
MIT para el cual se desarrolló este libro.
Capítulo 8. Clases y programación orientada a 1

def gradeReport(curso):
"""Asume que el curso es del tipo
Calificaciones""" report = ''
para s en
curso.getStudents(): tot
= 0.0
numCalificaciones = 0
para g en
curso.getGrades(s): tot
+= g
numGrades +=
1 intento:
promedio =
tot/numCalificaciones
informe = informe +
'\n'\
+ str(s) + 'La calificación media es ' +
str(promedio) excepto ZeroDivisionError:
informe = informe + '\n'\
+ str(s) + 'no tiene calificaciones'
informe de retorno

ug1 = UG('Jane Doe', 2014)


ug2 = UG('John Doe', 2015)
ug3 = UG('David Henry',
2003) g1 = Grad('Billy
Buckner') g2 = Grad('Bucky
F. Dent') seiscientos =
Grados()
seiscientos.añadirEstudiant
e(ug1)
seiscientos.añadirEstudiant
e(ug2)
seiscientos.añadirEstudiant
e(g1)
Figura 8.6 Generación de un informe de calificaciones

Cuando se ejecuta, el có digo de la figura se imprime


La nota media de Jane Doe es
75.0 La nota media de John
Doe es 75.0 David Henry no
tiene notas
La nota media de Billy Buckner es
50,0 La nota media de Bucky F.
Dent es 87,5

Hay dos conceptos importantes en el corazó n de la programació n orientada a


objetos. La primera es la idea de encapsulació n. Con esto nos referimos al
agrupamiento de atributos de datos y los mé todos para operar con ellos. Por
ejemplo, si escribimos
Rafael = MITPersona()

podemos usar la notació n de puntos para acceder a atributos como la edad y el


nú mero de identificació n de Rafael.

El segundo concepto importante es la ocultació n de informació n. Esta es una de las


claves de la modularidad. Si aquellas partes del programa que utilizan una clase (es
decir, los clientes de la clase) se basan ú nicamente en las especificaciones de los
mé todos de la clase, un programador que implementa la clase es libre de cambiar la
implementació n de la clase (por ejemplo, para
1 Capítulo 8. Clases y programación orientada a

mejorar la eficiencia) sin preocuparse de que el cambio rompa el có digo que usa la
clase.

Algunos lenguajes de programació n (Java y C++, por ejemplo) proporcionan


mecanismos para hacer cumplir la ocultació n de informació n. Los programadores
pueden hacer que los atributos de datos de una clase sean invisibles para los clientes
de la clase y, por lo tanto, requieren que se acceda a los datos solo a travé s de los
mé todos del objeto. Entonces, por ejemplo, podríamos obtener el idNum asociado
con Rafael ejecutando Rafael.getIdNum() pero no escribiendo Rafael.idNum.

Desafortunadamente, Python no proporciona mecanismos para hacer cumplir la


ocultació n de informació n. No hay forma de que el implementador de una clase
restrinja el acceso a los atributos de las instancias de la clase. Por ejemplo, un
cliente de una Persona puede escribir la expresió n Rafael.lastName en lugar de
Rafael.getLastName().

¿Por qué es esto desafortunado? Porque el có digo del cliente se basa en algo que no
forma parte de la especificació n de Person y, por lo tanto, está sujeto a cambios. Si se
cambiara la implementació n de Person, por ejemplo, para extraer el apellido cada
vez que se solicite en lugar de almacenarlo en una variable de instancia, entonces el
có digo del cliente ya no funcionaría.

Python no solo permite que los programas lean instancias y variables de clase
desde fuera de la definició n de clase, sino que tambié n permite que los
programas escriban estas variables. Entonces, por ejemplo, el có digo
Rafael.cumpleañ os = '21/8/50' es perfectamente legal. Esto conduciría a un error
de tipo de tiempo de ejecució n, si Rafael.getAge se invocara má s tarde en el
cá lculo. Incluso es posible crear variables de instancia desde fuera de la
definició n de clase. Por ejemplo, Python no se quejará si la declaració n de
asignació n me.age = Rafael.getAge() ocurre fuera de la definició n de la clase.

Si bien esta verificació n semá ntica está tica dé bil es una falla en Python, no es
una falla fatal. Un programador disciplinado puede simplemente seguir la regla
sensata de no acceder directamente a los atributos de los datos desde fuera de la
clase en la que está n definidos, como hacemos en este libro.

8.3.1 Generadores
Un riesgo percibido de la ocultació n de informació n es que evitar que los programas
cliente accedan directamente a las estructuras de datos críticos conduce a una
pé rdida de eficiencia inaceptable. En los primeros días de la abstracció n de datos,
muchos estaban preocupados por el costo de introducir llamadas a
funciones/métodos extrañ os. La tecnología de compilació n moderna hace que esta
preocupació n sea discutible. Un problema má s grave es que los programas cliente se
verá n obligados a utilizar algoritmos ineficientes.

Considere la implementació n de gradeReport en la Figura 8.6. La invocació n de


Course.getStudents crea y devuelve una lista de tamañ o n, donde n es el nú mero de
estudiantes. Probablemente esto no sea un problema para un libro de calificaciones de
una sola clase, pero imagine llevar un registro de las calificaciones de 1,7 millones de
estudiantes de secundaria que toman el SAT. Crear una nueva lista de ese tamañ o
cuando la lista ya existe es una ineficiencia significativa. Una solució n es abandonar la
abstracció n y permitir que gradeReport acceda directamente a la variable de instancia
curso.estudiantes, pero eso violaría la ocultació n de informació n. Afortunadamente, hay
una solució n mejor.
Capítulo 8. Clases y programación orientada a 1

El có digo de la Figura 8.7 reemplaza la funció n obtenerEstudiantes en la clase Calificaciones


con una funció n que usa un tipo de declaració n que aú n no hemos usado: una declaració n de
rendimiento.

def obtenerEstudiantes(self):
Figura 8.7 Nueva versión de obtenerEstudiantes
"""Regresar a los estudiantes en el libro de
Cualquier calificaciones unon que
definició n de funció a lacontenga
vez""" una
si no es self.isSorted:
declaració n de rendimiento se
self.students.sort
trata de manera especial. La presencia de yield le dice al sistema de Python que la
() self.isSorted =
funció n es un generador.
True Los generadores se usan normalmente junto con
parafor.40
declaraciones s en
self.students:
Al comienzo de la primera iteració n de un ciclo for, el intérprete comienza a ejecutar
el có digo en el cuerpo del generador. Se ejecuta hasta la primera vez que se ejecuta
una declaració n de rendimiento, momento en el que devuelve el valor de la
expresió n en la declaració n de rendimiento. En la siguiente iteració n, el generador
reanuda la ejecució n inmediatamente despué s del rendimiento, con todas las
variables locales vinculadas a los objetos a los que estaban vinculadas cuando se
ejecutó la declaració n de rendimiento, y nuevamente se ejecuta hasta que se ejecuta
una declaració n de rendimiento. Continú a haciendo esto hasta que se queda sin
có digo para ejecutar o ejecuta una declaració n de devolució n, momento en el que se
sale del ciclo.

La versió n de getStudents en la Figura 8.7 permite a los programadores usar un ciclo


for para iterar sobre los estudiantes en objetos de tipo Calificaciones de la misma
manera que pueden usar un ciclo for para iterar sobre elementos de tipos integrados
como lista. Por ejemplo, el có digo
libro = Calificaciones()
libro.addStudent(Grad('Julie'))b
ook.addStudent(Grad('Charlie'))
para s en book.getStudents():
imprimir

huellas dactilares
julie
charlie

Así, el ciclo de la figura 8.6 que comienza con


para s en curso.getStudents():

no tiene que modificarse para aprovechar la versió n de la clase Grades que contiene
la nueva implementació n de getStudents. El mismo bucle for puede iterar sobre los
valores proporcionados por getStudents independientemente de si getStudents
devuelve una lista de valores o genera un valor a la vez. Generando un valor en

40Esta explicació n de los generadores es un poco simplista. Para comprender


completamente los generadores, debe comprender la forma en que se implementan los
iteradores integrados en Python, que no se trata en este libro.
1 Capítulo 8. Clases y programación orientada a

un tiempo será má s eficiente, porque no se creará una nueva lista que contenga a los
estudiantes.

8.4 Hipotecas, un ejemplo extendido


Un colapso en los precios de la vivienda en los EE. UU. ayudó a desencadenar
una grave crisis econó mica en el otoñ o de 2008. Uno de los factores que
contribuyeron fue que muchos propietarios habían tomado hipotecas que
terminaron teniendo consecuencias inesperadas.41

Al principio, las hipotecas eran bestias relativamente simples. Uno pidió dinero
prestado a un banco e hizo un pago fijo cada mes durante la vigencia de la hipoteca,
que generalmente oscilaba entre quince y treinta añ os. Al final de ese período, el
banco había devuelto el pré stamo inicial (el capital) má s los intereses, y el
propietario era dueñ o de la casa “libre y limpio”.

Hacia finales del siglo XX, las hipotecas comenzaron a ser mucho má s complicadas.
Las personas podían obtener tasas de interés má s bajas pagando "puntos" en el
momento en que adquirieron la hipoteca. Un punto es un pago en efectivo del 1% del
valor del préstamo. Las personas podían tomar hipotecas que eran de "solo interé s"
por un período de tiempo. Es decir, durante un nú mero de meses al inicio del
pré stamo el prestatario pagaba ú nicamente los intereses devengados y nada de
principal. Otros pré stamos involucraron tasas mú ltiples. Por lo general, la tasa inicial
(llamada "tasa teaser") era baja y luego aumentaba con el tiempo. Muchos de estos
pré stamos eran de tasa variable: la tasa a pagar despué s del período inicial variaría
dependiendo de algú n índice destinado a reflejar el costo para el prestamista de
tomar prestado en el mercado mayorista de cré dito.42

En principio, dar a los consumidores una variedad de opciones es algo bueno. Sin
embargo, los proveedores de préstamos sin escrú pulos no siempre tuvieron
cuidado de explicar completamente las posibles implicaciones a largo plazo de
las diversas opciones, y algunos prestatarios tomaron decisiones que resultaron
tener consecuencias nefastas.

Construyamos un programa que examine los costos de tres tipos de pré stamos:

 Una hipoteca de tasa fija sin puntos,


 Una hipoteca de tasa fija con puntos, y
 Una hipoteca con una tasa teaser inicial seguida de una tasa má s alta
durante la duració n.

El objetivo de este ejercicio es proporcionar algo de experiencia en el desarrollo


incremental de un conjunto de clases relacionadas, no convertirlo en un experto en
hipotecas.

Estructuraremos nuestro có digo para incluir una clase de hipoteca y subclases


correspondientes a cada uno de los tres tipos de hipotecas enumerados
anteriormente.

41En este contexto, vale la pena recordar la etimología de la palabra hipoteca. El American
Heritage Dictionary of the English Language remonta la palabra a las antiguas palabras
francesas para muerto (mort) y prenda (gage). (Esta derivació n también explica por qué la "t"
en el medio de la hipoteca es muda).
42La tasa de oferta interbancaria de Londres (LIBOR) es probablemente el índice má s
utilizado.
Capítulo 8. Clases y programación orientada a 1

La figura 8.8 contiene la clase abstracta Hipoteca. Esta clase contiene mé todos
que son compartidos por cada una de las subclases, pero no está diseñ ado para
ser instanciado directamente.

La funció n findPayment en la parte superior de la figura calcula el tamañ o del pago


mensual fijo necesario para pagar el pré stamo, incluidos los intereses, al final de su
plazo. Lo hace usando una conocida expresió n de forma cerrada. Esta expresió n no es
difícil de derivar, pero es mucho má s fá cil simplemente buscarla y es má s probable que
sea correcta que una derivada en el acto.

Cuando su có digo incorpore fó rmulas que haya buscado, asegú rese de que:

 Ha tomado la fó rmula de una fuente confiable. Analizamos varias fuentes


acreditadas, todas las cuales contenían fó rmulas equivalentes.
 Entiende completamente el significado de todas las variables en la fó rmula.
 Prueba su implementació n con ejemplos tomados de fuentes confiables.
Despué s de implementar esta funció n, la probamos comparando nuestros
resultados con los resultados proporcionados por una calculadora
disponible en la Web.

Figura 8.8Hipotecaclase base


def buscarPago(préstamo, r, m):
Mirando"""Supone: préstamo
ainit, vemos y r las
que todas son instancias
flotantes, de
m Hipoteca
an int tendrán variables
Devuelve el pago mensual de una hipoteca de tamaño
de instancia correspondientes al monto inicial del préstamo, la tasa de
de préstamo
interés mensual, a una tasa
la duración mensual en
del préstamo de meses,
r durante
una mlista de pagos que se
meses"""
han realizado al comienzo de cada mes (la lista comienza con 0.0, ya que no
se han devolver
realizadopréstamo*((r*(1+r)**m)/((1+r)**m
pagos al inicio del primer mes), una- 1))
lista con el saldo del
préstamo que está
clase Hipoteca(objeto):
"""Clase abstracta para construir diferentes tipos de
hipotecas""" def __init__(self, préstamo, annRate, meses):
"""Crear una nueva
hipoteca""" self.loan =
préstamo
self.rate =
annRate/12.0
self.months = meses
self.paid = [0.0]
self.deber = [loan]
self.pago = findPayment(préstamo, self.rate, meses)
self.legend = Ninguno #descripción de la hipoteca
def
realizarPago(auto)
: """Hacer un
pago"""
auto.pago.append(auto.pago)
reducción = auto.pago - auto.deuda[-
1]*auto.tarifa auto.deuda.append(auto.deuda[-1] -
reducción)
1 Capítulo 8. Clases y programación orientada a

pendiente al comienzo de cada mes, la cantidad de dinero a pagar cada mes


(inicializado utilizando el valor devuelto por la funció n findPayment), y una
descripció n de la hipoteca (que inicialmente tiene un valor de Ninguno). Se espera
que la operació n init de cada subclase de Mortgage comience llamando
Hipoteca. en eso, y luego para inicializarauto.leyendaa una descripció n
apropiada de esa subclase.

El método makePayment se utiliza para registrar los pagos de la hipoteca. Parte de


cada pago cubre el monto de los intereses adeudados sobre el saldo pendiente del
pré stamo y el resto del pago se utiliza para reducir el saldo del pré stamo. Es por eso
que makePayment actualiza tanto los pagos propios como los pagos propios.

El método getTotalPaid utiliza la funció n suma integrada de Python, que devuelve la


suma de una secuencia de nú meros. Si la secuencia no contiene un nú mero, se genera
una excepció n.

La figura 8.9 contiene clases que implementan dos tipos de hipoteca. Cada una de
estas clases anula init y hereda los otros tres mé todos de Mortgage.

Figura 8.9 Clases de hipotecas de tasa fija


clase Fijo(Hipoteca):
La figura 8.10
def contiene una tercera
__init__(self, subclaser,
préstamo, demeses):
Hipoteca. La claseDosTasatrata la
Hipoteca.__init__(self, préstamo, r,uno
hipoteca como la concatenació n de dos pré stamos, cada meses)
a una tasa de interé s
self.legend = 'Fixed, ' + str(r*100) + '%'
diferente. (Desdeauto.pagadose inicializa con un0.0, contiene un elemento má s que
el nú mero
clasedeFixedWithPts(hipoteca):
pagos que se han realizado. Es por eso hacer el
def __init__(self,
pagocomparalen(pago propio)préstamo, r, meses,+ pts):
aself.teaserMeses 1.).
Hipoteca.__init__(self, préstamo, r, meses)
self.pts = pts
self.paid = [prestamo*(pts/100.0)]
self.leyenda = 'Fijo, ' + str(r*100) + '%, '\
+ str(pts) + 'puntos'
Capítulo 8. Clases y programación orientada a 1

clase TwoRate(Hipoteca):
def __init__(self, préstamo, r, meses, teaserRate, teaserMonths):
Hipoteca.__init__(self, préstamo, teaserRate, meses)
self.teaserMonths = teaserMonths
self.teaserRate = teaserRate
self.nextRate = r/12.0
self.legend =
str(teaserRate*100)\
+ '% para ' + str(self.teaserMonths)\
+ ' meses, luego ' + str(r*100) +
'%' def realizarPago(auto):
if len(self.paid) == self.teaserMonths + 1:
self.rate = self.nextRate
auto.pago = findPayment(auto.deuda[-1], auto.tasa,
self.months - self.teaserMonths)

Figura 8.10 Hipoteca con tasa teaser

La figura 8.11 contiene una funció n que calcula e imprime el costo total de cada
tipo de hipoteca para un conjunto de pará metros de muestra. Comienza creando
una hipoteca de cada tipo. A continuació n, realiza un pago mensual de cada uno
durante un nú mero determinado de añ os. Finalmente, imprime el monto total
de los pagos realizados por cada pré stamo.

Figura 8.11 Evaluar hipotecas


def compareMortgages(amt, years, fixedRate, pts, ptsRate,
Tenga en cuenta que usamos varRate1, varRate2,
palabras clave en lugar de argumentos posicionales en
varMeses): totMeses = años*12
la invocació n de compareMortgages. Hicimos esto porque compareMortgages tiene
fijo1 = Fijo(cantidad, tasafija, totMeses)
una gran cantidad
fijo2 de pará metros formales
= FijoConPtos(amt, y eltotMonths,
ptsRate, uso de argumentos
pts) de palabras clave
hace que sea má s fá cil garantizar que estamos proporcionando
dosRate = TwoRate(importe, varRate2, totMonths, varRate1, los valores reales
varMonths) morts = [fixed1,
previstos para cada uno de los formales. fixed2, twoRate]
for m in
range(totMonths):
for mort in morts:
mort.makePayment
() para m en morts:
imprimir m
print ' Pagos totales = $' + str(int(m.getTotalPaid()))

compareMortgages(amt=200000, years=30, fixedRate=0.07,


pts = 3,25, ptsRate=0,05, varRate1=0,045,
varRate2=0,095, varMonths=48)
1 Capítulo 8. Clases y programación orientada a

Cuandoel có digo enSe ejecuta la Figura 8.11, se imprime

Fijo, 7,0%
Pagos totales = $479017
Fijo, 5.0%, 3.25 puntos
Pagos totales = $393011 4.5%
por 48 meses, luego 9.5%
Pagos totales = $551444

A primera vista, los resultados parecen bastante concluyentes. El pré stamo de tasa
variable es una mala idea (para el prestatario, no para el banco) y el préstamo de
tasa fija con puntos cuesta menos. Sin embargo, es importante tener en cuenta que
el costo total no es la ú nica medida por la cual se deben juzgar las hipotecas. Por
ejemplo, un prestatario que espera tener mayores ingresos en el futuro puede estar
dispuesto a pagar má s en los ú ltimos añ os para disminuir la carga de los pagos al
principio.

Esto sugiere que en lugar de mirar un solo nú mero, deberíamos mirar los pagos a
lo largo del tiempo. Esto, a su vez, sugiere que nuestro programa debería producir
parcelas diseñ adas para mostrar có mo se comporta la hipoteca a lo largo del
tiempo. Lo haremos en la Secció n 11.2.
9A INTRODUCCIÓN SIMPLISTA A LA ALGORITMICA
COMPLEJIDAD

Lo má s importante a tener en cuenta al diseñ ar e implementar un programa es que


debe producir resultados en los que se pueda confiar. Queremos que nuestros
saldos bancarios se calculen correctamente. Queremos que los inyectores de
combustible de nuestros automó viles inyecten cantidades adecuadas de
combustible. Preferiríamos que ni los aviones ni los sistemas operativos se cayeran.

A veces, el rendimiento es un aspecto importante de la correcció n. Esto es má s obvio


para los programas que necesitan ejecutarse en tiempo real. Un programa que
advierte a los aviones de posibles obstrucciones debe emitir la advertencia antes de
que se encuentren las obstrucciones. El rendimiento también puede afectar la
utilidad de muchos programas que no son en tiempo real. La cantidad de
transacciones completadas por minuto es una mé trica importante cuando se evalú a
la utilidad de los sistemas de bases de datos. Los usuarios se preocupan por el
tiempo necesario para iniciar una aplicació n en su telé fono. A los bió logos les
importa cuá nto tardan sus cá lculos de inferencia filogené tica.

Escribir programas eficientes no es fá cil. La solució n má s sencilla a menudo no es


la má s eficiente. Los algoritmos computacionalmente eficientes a menudo
emplean trucos sutiles que pueden dificultar su comprensió n. En consecuencia,
los programadores suelen aumentar la complejidad conceptual de un programa
en un esfuerzo por reducir su complejidad computacional. Para hacer esto de
una manera sensata, necesitamos entender có mo estimar la complejidad
computacional de un programa. Ese es el tema de este capítulo.

9.1 Pensando en la complejidad computacional


¿Có mo se debe responder a la pregunta "¿Cuá nto tiempo tardará en ejecutarse la
siguiente funció n?"
definición f(i):
"""Supone que i es un int y i >=
0""" respuesta = 1
mientras i >=
1:
respuesta
*= ii -= 1
devolver respuesta

Podríamos ejecutar el programa en alguna entrada y cronometrarlo. Pero eso no


sería particularmente informativo porque el resultado dependería de

1. la velocidad de la computadora en la que se ejecuta,


2. la eficiencia de la implementació n de Python en esa má quina, y
3. el valor de la entrada.
Solucionamos los dos primeros problemas utilizando una medida de tiempo má s
abstracta. En lugar de medir el tiempo en milisegundos, lo medimos en té rminos
del nú mero de pasos bá sicos ejecutados por el programa.
1 Capter9. ASsimplistaEntroducción

Para simplificar, utilizaremos una má quina de acceso aleatorio como modelo de


cá lculo. En una má quina de acceso aleatorio, los pasos se ejecutan
secuencialmente, uno a la vez.43 Un paso es una operació n que lleva una
cantidad fija de tiempo, como vincular una variable a un objeto, hacer una
comparació n, ejecutar una operació n aritmé tica o acceder a un objeto en la
memoria.

Ahora que tenemos una forma má s abstracta de pensar sobre el significado del
tiempo, pasamos a la cuestió n de la dependencia del valor de la entrada. Nos
ocupamos de eso alejá ndonos de expresar la complejidad del tiempo como un solo
nú mero y en su lugar relacioná ndolo con los tamañ os de las entradas. Esto nos
permite comparar la eficiencia de dos algoritmos hablando de có mo crece el tiempo
de ejecució n de cada uno con respecto al tamañ o de las entradas.

Por supuesto, el tiempo de ejecució n real de un algoritmo depende no solo del


tamañ o de las entradas sino tambié n de sus valores. Considere, por ejemplo, el
algoritmo de bú squeda lineal implementado por
def BúsquedaLineal(L,
x): for e in L:
si e == x:
devuelve
Verdadero
falso retorno

Suponga que L tiene una longitud de un milló n de elementos y considere la llamada


búsqueda lineal (L, 3). Si el primer elemento deLes3,búsqueda
linealregresará Verdaderocasi inmediatamente. Por otro lado, si3no está
dentroL,búsqueda linealtendrá que examinar el milló n de elementos antes de
regresarFALSO.

En general, hay tres casos amplios en los que pensar:

 El mejor tiempo de ejecució n es el tiempo de ejecució n del algoritmo


cuando las entradas son lo má s favorables posible. Es decir, el tiempo de
ejecució n en el mejor de los casos es el tiempo de ejecució n mínimo sobre
todas las entradas posibles de un tamañ o dado. Parabúsqueda lineal, el
tiempo de ejecució n en el mejor de los casos es independiente del tamañ o
deL.
 De manera similar, el tiempo de ejecució n en el peor de los casos es el
tiempo de ejecució n má ximo sobre todas las entradas posibles de un
tamañ o dado. Parabúsqueda lineal, el tiempo de ejecució n del peor de
los casos es lineal en el tamañ o de la lista.
 Por analogía con las definiciones del tiempo de ejecució n del mejor y el peor
caso, el tiempo de ejecució n del caso promedio (tambié n llamado caso
esperado) es el tiempo de ejecució n promedio sobre todas las entradas
posibles de un tamañ o dado. Alternativamente, si uno tiene alguna
informació n a priori sobre la distribució n de los valores de entrada (por
ejemplo, que90%del tiempoXes enL), se puede tener eso en cuenta.
La gente suele centrarse en el peor de los casos. Todos los ingenieros comparten un
artículo de fe comú n, la Ley de Murphy: si algo puede salir mal, saldrá mal. El peor de
los casos proporciona un límite superior en el tiempo de ejecució n. Esto es
fundamental en situaciones en las que existe una limitació n de tiempo en cuanto a la
duració n de un cá lculo. No lo es

43Un modelo má s preciso para las computadoras de hoy podría ser una má quina de
acceso aleatorio paralelo. Sin embargo, eso agrega una complejidad considerable al
Capter9.Una introducción simplista a la complejidad
aná lisis algorítmico y, a menudo, no hace una diferencia cualitativa importante en la
1
respuesta.
1 Capter9. ASsimplistaEntroducción

lo suficientemente bueno como para saber que "la mayoría de las veces" el sistema
de control de trá fico aé reo advierte sobre colisiones inminentes antes de que
ocurran.

Veamos el tiempo de ejecució n en el peor de los casos de una implementació n


iterativa de la funció n factorial
def hecho(n):
"""Supone que n es un número
natural Devuelve n!"""
respuesta =
1 mientras
que n > 1:
respuesta *=
nn -= 1
devolver respuesta

El nú mero de pasos necesarios para ejecutar este programa es algo así como 2 (1
para la instrucció n de asignació n inicial y uno para la devolució n) + 5n (contando 1
paso para la prueba en el ciclo while, 2 pasos para la primera instrucció n de
asignació n en el ciclo while) y 2 pasos para la segunda declaració n de asignació n en
el ciclo). Entonces, por ejemplo, si n es 1000, la funció n ejecutará aproximadamente
5002 pasos.

Debería ser inmediatamente obvio que a medida que n crece, preocuparse por la
diferencia entre 5n y 5n+2 es un poco tonto. Por esta razó n, normalmente
ignoramos las constantes aditivas cuando razonamos sobre el tiempo de
ejecució n. Las constantes multiplicativas son má s problemá ticas. ¿Debería
importarnos si el cá lculo toma 1000 pasos o 5000 pasos? Los factores
multiplicativos pueden ser importantes.
Si un motor de bú squeda tarda medio segundo o 2,5 segundos en atender una
consulta puede ser la diferencia entre si las personas usan ese motor de bú squeda
o acuden a un competidor.

Por otro lado, cuando uno está comparando dos algoritmos diferentes, a menudo
ocurre que incluso las constantes multiplicativas son irrelevantes. Recuerde que en
el Capítulo 3 vimos dos algoritmos, enumeració n exhaustiva y bú squeda de
bisecció n, para encontrar una aproximació n a la raíz cuadrada de un nú mero de
coma flotante. Las funciones basadas en cada uno de estos algoritmos se muestran
en la Figura 9.1 y la Figura 9.2.

Figura
def9.1 Uso de la enumeración exhaustiva
squareRootExhaustive(x, epsilon): para aproximar la raíz cuadrada
"""Asume que x y epsilon son flotantes positivos & epsilon
< 1 Devuelve ay tal que y*y está dentro de epsilon de
x"""
paso = épsilon**2
respuesta = 0.0
while abs(ans**2 - x) >= épsilon y ans*ans <= x:
ans += paso
si ans*ans > x:
aumentar
Capter9.Una introducción simplista a la complejidad 1

def raízcuadradaBi(x, épsilon):


"""Asume que x y epsilon son flotantes positivos & epsilon
< 1 Devuelve ay tal que y*y está dentro de epsilon de
x"""
bajo = 0.0
alto = max(1.0, x) y
= (alto + bajo)/2.0
while abs(respuesta**2 - x) >=
épsilon: si respuesta**2 <
x:
bajo =
y más:
alto = respuesta

Figura 9.2 Uso de la búsqueda de bisección para aproximar la raíz


cuadrada

Vimos que la enumeració n exhaustiva era tan lenta que resultaba poco prá ctica
para muchas combinaciones de x y é psilon. Por ejemplo, evaluar
squareRootExhaustive(100, 0.0001) requiere aproximadamente mil millones de
iteraciones del bucle. Por el contrario, la evaluació n de squareRootBi(100, 0.0001)
requiere aproximadamente veinte iteraciones de un ciclo while un poco má s
complejo. Cuando la diferencia en el nú mero de iteraciones es tan grande, en
realidad no importa cuá ntas instrucciones haya en el ciclo. Es decir, las constantes
multiplicativas son irrelevantes.

9.2 Notación asintótica


Usamos algo llamado notació n asintó tica para proporcionar una forma formal de
hablar sobre la relació n entre el tiempo de ejecució n de un algoritmo y el tamañ o de
sus entradas. La motivació n subyacente es que casi cualquier algoritmo es lo
suficientemente eficiente cuando se ejecuta con entradas pequeñ as. Por lo general,
debemos preocuparnos por la eficiencia del algoritmo cuando se ejecuta con
entradas muy grandes. Como proxy de "muy grande", la notació n asintó tica describe
la complejidad de un algoritmo a medida que el tamañ o de sus entradas se acerca al
infinito.

Consideremos, por ejemplo, el có digo


definición f(x):
"""Asumir que x es un int >
0""" ans = 0
#Loop que toma un tiempo
constante para i en el rango
(1000):
respuesta += 1
imprime 'Número de adiciones hasta
ahora', y #Loop que toma tiempo x
para i en el rango
(x): ans += 1
imprime 'Número de adiciones hasta
ahora', y #Los bucles anidados toman
tiempo x ** 2
para i en rango(x):
para j en
rango(x):
respuesta += 1
respuesta += 1
imprimir 'Número de adiciones hasta
ahora', ans return ans
1 Capter9. ASsimplistaEntroducción

Si se supone que cada línea de có digo tarda una unidad de tiempo en ejecutarse, el
tiempo de ejecució n de esta funció n se puede describir como 1000 + x + 2x2. La
constante 1000 corresponde al nú mero de veces que se ejecuta el primer bucle. El
té rmino x corresponde al nú mero de veces que se ejecuta el segundo bucle.
Finalmente, el término 2x2 corresponde al tiempo empleado en ejecutar las dos
sentencias en el bucle for anidado. En consecuencia, la llamada f(10) imprimirá
Número de adiciones hasta ahora
1000 Número de adiciones hasta
ahora 1010 Número de adiciones
hasta ahora 1210

Para pequeñ os valores deXel término constante domina. SiXes10,1000del1210los pasos se


contabilizan en el primer bucle. Por otro lado, si Xes1000, cada uno de los dos primeros
bucles representa solo0,05%de los pasos CuandoXes1,000,000, el primer bucle dura
aproximadamente0.00000005%del tiempo total y el segundo bucle sobre0.00005%.Una
completa2,000,000,000,000del2,000,001,001,000los pasos están en el cuerpo del
interiorparabucle.

Claramente, podemos obtener una noció n significativa de cuá nto tiempo tardará
este có digo en ejecutarse con entradas muy grandes considerando solo el ciclo
interno, es decir, el componente cuadrá tico. ¿Deberíamos preocuparnos por el hecho
de que este ciclo toma 2x2 pasos en lugar de x2 pasos? Si su computadora ejecuta
aproximadamente 100 millones de pasos por segundo, evaluar f tomará alrededor
de 5,5 horas. Si pudiéramos reducir la complejidad a x2 pasos, tomaría alrededor de
2,25 horas. En cualquier caso, la moraleja es la misma: probablemente deberíamos
buscar un algoritmo má s eficiente.

Este tipo de aná lisis nos lleva a utilizar las siguientes reglas generales para describir
la complejidad asintó tica de un algoritmo:

 Si el tiempo de ejecució n es la suma de varios términos, qué dese con el


que tenga la mayor tasa de crecimiento y elimine los demá s.
 Si el té rmino restante es un producto, elimine cualquier constante.

La notació n asintó tica má s comú nmente usada se llama notació n “Big O”.44 La
notació n Big O se usa para dar un límite superior en el crecimiento asintó tico (a
menudo llamadoel orden de crecimiento) de una funció n. Por ejemplo, la fó rmula f(x) 
O(x2) significa que la funció n f no crece má s rá pido que el polinomio cuadrá tico x2, en un
sentido asintó tico.

Nosotros, como muchos informá ticos, a menudo abusamos de la notació n Big O al


hacer declaracionescomo, "la complejidad de f(x) es O(x2)". Con esto queremos decir que en el
peor de los casos f tomará O(x2) pasos para ejecutarse. La diferencia entre una funció n que está
“en O(x2)” y “que está en O(x2)” es sutil pero importante. Decir que f(x)  O (x2) no impide que el
tiempo de ejecució n de f en el peor de los casos sea considerablemente menor que O(x2).

44Lafrase "Big O" fue introducida en este contexto por el científico informá tico Donald
Knuth en la década de 1970. Eligió la letra griega Omicron porque los teó ricos de los
nú meros habían usado esa letra desde finales del siglo XIX. elsiglo para denotar un
concepto relacionado.
Capter9.Una introducción simplista a la complejidad 1

Quié n y dó ndenorteNosotros decimos esof(x) es O(x2),estamos dando a entender queX2


esun límite superior e inferior en el tiempo de ejecució n del peor de los casos asintó ticos. Esto
se llama un límite estrecho.45

9.3 Algunas Clases de Complejidad Importantes


Algunas de las instancias má s comunes de Big O se enumeran a continuació n. En
cada caso, n es una medida del tamañ o de las entradas a la funció n.

 O(1)denota un tiempo de ejecució n constante.

 O (registro n)denota tiempo de ejecució n logarítmico.

 En)denota tiempo de ejecució n lineal.

 O(n registro n)denota un tiempo de ejecució n logarítmico lineal.

 O(nk) denotarstiempo de ejecució n del polinomio. Observe que k es una constante.

 O(cn) denotarstiempo de ejecució n exponencial. Aquí se eleva una constante a una


potencia basada en el tamañ o de la entrada.

9.3.1 Complejidad constante


Esto indica que la complejidad asintó tica es independiente de las entradas. Hay
muy pocos programas interesantes en esta clase, pero todos los programas tienen
piezas (por ejemplo, averiguar la longitud de una lista de Python o multiplicar dos
nú meros de coma flotante) que encajan en esta clase. El tiempo de ejecució n
constante no implica que no haya bucles o llamadas recursivas en el có digo, pero sí
implica que el nú mero de iteraciones o llamadas recursivas es independiente del
tamañ o de las entradas.

9.3.2 Complejidad logarítmica


Tales funciones tienen una complejidad que crece con el logaritmo de al menos una
de las entradas. La bú squeda binaria, por ejemplo, es logarítmica en la longitud de
la lista que se busca. (Veremos la bú squeda binaria y analizaremos su complejidad
en el pró ximo capítulo.) Por cierto, no nos importa la base del logaritmo, ya que la
diferencia entre usar una base y otra es simplemente una constante
multiplicativafactor. Por ejemplo, O(log2(x)) = O(log2(10)*log10(x)). Hay muchas funciones
interesantes con complejidad logarítmica. Considerar

45Los miembros má s pedantes de la comunidad informá tica utilizan Big Theta, Θ, en lugar
de Big O para esto.
1 Capter9. ASsimplistaEntroducción

def intToStr(i):
"""Supone que i es un int no negativo
Devuelve una representación de cadena decimal de
i""" dígitos = '0123456789'
si i == 0:
devuelve
'0'
resultado =
'' mientras i
> 0:
resultado = dígitos[i%10] +
resultado i = i//10
resultado devuelto

Dado que no hay llamadas a funciones o mé todos en este có digo, sabemos que solo
tenemos que observar los bucles para determinar la clase de complejidad. Solo hay
un bucle, por lo que lo ú nico que debemos hacer es caracterizar el nú mero de
iteraciones. Eso se reduce a la cantidad de veces que uno puede dividir i entre 10.
Entonces, la complejidad de intToStr es O(log(i)).

¿Qué pasa con la complejidad de


def sumarDigitos(n):
“““Supone que n es un int no negativo
Devuelve la suma de los dígitos en
n"""
cadenaRep = intToStr(n)
val = 0
para c en
cadenaRep: val
+= int(c)
valor de retorno

La complejidad de convertirnortea una cadena esO(registro(n))yintToStrdevuelve una


cadena de longitudO(registro(n)). Elparase ejecutará el bucleO(len(cadenaRep))veces, es
decir,O(registro(n))veces. Reuniéndolo todo, y suponiendo que un cará cter que
representa un dígito se puede convertir en un nú mero entero en un tiempo constante,
el programa se ejecutará en un tiempo proporcional aO(registro(n)) + O(registro(n)), lo que
hace queO(registro(n)).

9.3.3 Complejidad lineal


Muchos algoritmos que manejan listas u otros tipos de secuencias son lineales
porque tocan cada elemento de la secuencia una cantidad constante (mayor que
0) de veces. Considere, por ejemplo,
def sumarDigitos(s):
"""Asume que s es una cadena cada uno de los cuales es
un dígito decimal.
Devuelve un int que es la suma de los dígitos en
s""" val = 0
para c en s:
valor +=
int(c) devuelve
valor

Esta funció n es lineal en la longitud de s, es decir, O(len(s)), suponiendo


nuevamente que un cará cter que representa un dígito se puede convertir en un
nú mero entero en tiempo constante.

Por supuesto, un programa no necesita tener un ciclo para tener complejidad lineal.
Capter9.Una introducción simplista a la complejidad 1

Considerar
factorial def(x):
"""Asume que x es un int positivo
Devuelve x!"""
si x == 1:
devuelve
1
demás:
devuelve x*factorial(x-1)

No hay bucles en este có digo, por lo que para analizar la complejidad necesitamos
averiguar cuá ntas llamadas recursivas se realizan. La serie de llamadas es
simplementefactoriales(x),factoriales(x-1),factoriales(x-2),...,
factoriales(1). La longitud de esta serie, y por lo tanto la complejidad de la funció n,
esBuey).

Hasta ahora, en este capítulo solo hemos analizado la complejidad temporal de


nuestro có digo. Esto está bien para algoritmos que usan una cantidad constante de
espacio, pero esta implementació n de factorial no tiene esa propiedad. Como
discutimos en el Capítulo 4, cada llamada recursiva de factorial hace que se asigne un
nuevo marco de pila, y ese marco continú a ocupando memoria hasta que regresa la
llamada. En la má xima profundidad de recursividad, este có digo habrá asignado x
marcos de pila, por lo que la complejidad del espacio es O(x).

El impacto de la complejidad del espacio es má s difícil de apreciar que el impacto de


la complejidad del tiempo. Si un programa tarda uno o dos minutos en completarse,
es bastante visible para el usuario, pero si utiliza un megabyte o dos megabytes de
memoria, es en gran parte invisible para los usuarios. Esta es la razó n por la cual las
personas suelen prestar má s atenció n a la complejidad del tiempo que a la
complejidad del espacio. La excepció n ocurre cuando un programa necesita má s
espacio del que está disponible en la memoria principal de la má quina en la que se
ejecuta.

9.3.4 Complejidad log-lineal


Esto es un poco má s complicado que las clases de complejidad que hemos visto
hasta ahora. Implica el producto de dos términos, cada uno de los cuales depende
del tamañ o de las entradas. Es una clase importante, porque muchos algoritmos
prá cticos son logarítmicos lineales. El algoritmo logarítmico lineal má s utilizado es
probablemente la ordenació n por fusió n, que es O(n log(n)), donde n es la longitud
de la lista que se ordena. Veremos ese algoritmo y analizaremos su complejidad en
el pró ximo capítulo.

9.3.5 Complejidad polinomial


Los algoritmos polinó micos má s utilizados son los cuadrá ticos, es decir, su
complejidad crece con el cuadrado del tamañ o de su entrada. Considere, por
ejemplo, la funció n de la figura 9.3, que implementa una prueba de subconjunto.
1 Capter9. ASsimplistaEntroducción

def esSubconjunto(L1, L2):


"""Asume que L1 y L2 son listas.
Devuelve True si cada elemento en L1 también
está en L2 y False en caso contrario."""
para e1 en L1:
emparejado =
Falso para e2
en L2:
si e1 == e2:
emparejado =
ruptura
verdadera
si no

Figura 9.3 Implementación de prueba de subconjunto

Cada vez que se alcanza el bucle interior, se ejecuta O(largo(L2)veces. La funció n


ejecutará el bucle exterior.O(largo(L1))veces, por lo que se alcanzará el bucle
interiorO(largo(L1))veces. Por lo tanto, la complejidad
deesSubconjuntoesO(largo(L1)*largo(L2)).

Ahora considere la funció n de intersecció n en la Figura 9.4.

Figura 9.4
intersección Implementación
def (L1, L2): de la intersección de listas
"""Supone: L1 y L2 son listas
El tiempo de ejecució
Devuelve n deuna
la parte
listaque crea
que eslala
lista que puede contener
intersección de L1 yduplicados
L2"""
es claramente O(len(L1)*len(L2)).
#Construir una lista Aque
primera vista,elementos
contenga parece quecomunes
la parte del có digo
que crea la tmp
lista =sin
[]duplicados es lineal en la longitud de tmp, pero no lo es. La
para e1 en L1:
prueba e not inpara
resulte2involucra
en potencialmente mirar cada elemento en result, y
por lo tanto es L2:
O(len(result)); en consecuencia, la segunda parte de la
implementació n essiO(len(tmp)*len(resultado)).
e1 == e2: Dado que las longitudes de result y
tmp.append(e
tmp está n limitadas por la longitud del menor de L1 y L2, y dado que ignoramos los
1)
té rminos aditivos,
#Construirla complejidad
una listadesin
intersect es O(len(L1)*len(L2)).
resultado de duplicados = []
9.3.6 Complejidad
para e en tmp: Exponencial
si e no está en
Como veremos má s adelante en este libro, muchos problemas importantes son
inherentemente exponenciales, es decir, resolverlos por completo puede
requerir un tiempo que es exponencial en el tamañ o de la entrada. Esto es
desafortunado, ya que rara vez vale la pena escribir un programa que tenga una
probabilidad razonablemente alta de tomar un tiempo exponencial para
ejecutarse.
Capter9.Una introducción simplista a la complejidad 1

Considere, por ejemplo, el có digo de la figura 9.5.

Figura 9.5 Generación


def getBinaryRep(n, del conjunto de potencia
numDigits):
"""Asume que n y numDigits son números enteros no
La funció ngenPowerset(L)
negativos.devuelve
Devuelve unauna
listacadena
una lista
de de listas que que
numDigits contiene
es todas
las combinacionesuna posibles de los elementos
representación de L.de
binaria Porn"""
ejemplo, siLes['a', 'b'], el
resultado
conjunto de potencia = ''una lista que contiene las listas[],['b'],['a'], y['a',
deLserá
mientras que n
'b']. > 0:
resultado = str(n%2) +
El algoritmo es un poco sutil. Considere una lista de n elementos. Podemos
resultado n = n//2
representar cualquier combinació n>de
if len(resultado) elementos por una cadena de n 0 y 1, donde
numDigits:
un 1 representa laaumentar
presenciaValueError
de un elemento ('noy hay
un 0 su ausencia. La combinació n que
no contiene elementos se representaría con unaen
suficientes dígitos') para i el de todos los 0, la
cadena
rango (numDigits - len (resultado)):
combinació n que resultado
contiene todos
= '0'los +elementos se representaría con una cadena de
todos los 1, la resultado
combinació devuelve
n que contiene solo el primer y el ú ltimo elemento se
representaría resultado
con 100... 001, etc. Por lo tanto, la generació n de todas las sublistas de
una lista L de longitud n se puede hacer de la siguiente manera:
def genPowerset(L):
1. generar """Supone
todonorteque L es binarios de bits. Estos son los nú meros de0a2n.
Nú meros
una lista
2. para cadaDevuelve
uno de una estos
lista
2n +1 numeros
de listas binarios, b, genere
que contiene todasuna lista
las combinaciones
seleccionando posibles de
aquellos elementos de Llos
queelementos
tienen deun índice
LEg, sia un1enb. Por ejemplo, siLes['a', 'b']ybes01, generar
correspondiente
L es
la lista['b'] . [1, 2] devolverá una lista con los
elementos [], [1], [2] y [1,2]."""
Intente ejecutar genPowerset
conjunto en una lista
de potencia = []que contenga las primeras diez letras del
for i bastante
alfabeto. Terminará in range(0, 2**len(L)):
rá pido y producirá una lista con 1024 elementos. A
binStr = getBinaryRep(i,
continuació n, intente ejecutar
len(L)) genPowerset
subconjunto = []en las primeras veinte letras del
alfabeto. Llevará má s de un poco de tiempo ejecutarse y devolverá una lista con
aproximadamente un milló n de elementos. Si intenta ejecutar genPowerset en las
veintiséis letras, probablemente se cansará de
1 Capter9. ASsimplistaEntroducción

esperando a que se complete, a menos que su computadora se quede sin memoria


tratando de construir una lista con decenas de millones de elementos. Ni siquiera
piense en intentar ejecutar genPowerset en una lista que contiene todas las letras
mayú sculas y minú sculas. El paso 1 del algoritmo genera nú meros binarios
O(2len(L)), por lo que el algoritmo es exponencial en len(L).

¿Significa esto que no podemos usar la computació n para abordar problemas


exponencialmente difíciles? Absolutamente no. Significa que tenemos que
encontrar algoritmos que proporcionen soluciones aproximadas a estos
problemas o que encuentren soluciones perfectas en algunas instancias del
problema. Pero ese es un tema para capítulos posteriores.

9.3.7 Comparaciones de clases de complejidad


Los siguientes grá ficos pretenden
transmitir una impresió n de las
implicaciones de que un algoritmo se
encuentre en una u otra de estas clases
de complejidad.

La grá fica de la derecha compara el


crecimiento de un algoritmo de
tiempo constante con el de un
algoritmo logarítmico.
Tenga en cuenta que el tamañ o de la
entrada tiene que alcanzar alrededor
de un milló n para que los dos se
crucen, incluso para la pequeñ a
constante de veinte. Cuando el
tamañ o de
la entrada es cinco millones, el tiempo requerido por un algoritmo logarítmico sigue siendo
bastante pequeñ o. La moraleja es
que los algoritmos logarítmicos son
casi tan buenos como los de tiempo
constante.

La grá fica de la izquierda ilustra la


gran diferencia entre los algoritmos
logarítmicos y los algoritmos lineales.
Observe que el eje y solo llega hasta
1000. Si bien necesitá bamos
observar entradas grandes para
apreciar la diferencia entre los
algoritmos de tiempo constante y
logarítmico, la diferencia entre
Los algoritmos de tiempo logarítmico y de tiempo lineal son evidentes incluso en
entradas pequeñ as. La gran diferencia en el rendimiento relativo de los algoritmos
logarítmicos y lineales no significa que los algoritmos lineales sean malos. De
hecho, la mayoría de las veces un algoritmo lineal es aceptablemente eficiente.

La grá fica de abajo ya la izquierda muestra que hay una diferencia significativa
entre O(n) y O(n log(n)). Dada la lentitud con la que crece log(n), esto puede parecer
un poco sorprendente, pero tenga en cuenta que es un factor multiplicativo.
También tenga en cuenta que en la mayoría de las situaciones prá cticas, O(n log(n))
es lo suficientemente rá pido como para ser ú til.
Capter9.Una introducción simplista a la complejidad 1

Por otro lado, como sugiere el diagrama de abajo ya la derecha, hay muchas
situaciones en las que una tasa de crecimiento cuadrá tica es prohibitiva. La curva
cuadrá tica crece tan rá pidamente que es difícil ver que la curva logarítmica lineal
es pareja en el grá fico.

Las dos ú ltimas tramas tratan sobre la complejidad exponencial.

En el grá fico de la izquierda, los nú meros a la izquierda del eje y van de 0,0 a 1,2.
Sin embargo, la notació n x1e301 en la parte superior izquierda significa que cada
marca en el eje y debe multiplicarse por 10301. Por lo tanto, los valores de y
trazados van de 0 a aproximadamente 1,1*10301. Sin embargo, parece casi como
si no hubiera curvas en el grá fico de la izquierda. Esto se debe a que una funció n
exponencial crece tan rá pidamente que, en relació n con el valor y del punto má s
alto (que determina la escala del eje y), los valores y de los puntos anteriores de
la curva exponencial (y todos los puntos de la curva cuadrá tica) son casi
indistinguible de 0.

El grá fico de la derecha aborda este problema mediante el uso de una escala
logarítmica en el eje y. Uno puede ver fá cilmente que los algoritmos exponenciales
no son prá cticos para todas las entradas excepto para las má s pequeñ as.

Observe, por cierto, que cuando se grafica en una escala logarítmica, una curva
exponencial aparece como una línea recta. Tendremos má s que decir sobre esto en
capítulos posteriores.
1 Capter9. ASsimplistaEntroducción

10 ALGUNOS ALGORITMOS Y DATOS SENCILLOS


ESTRUCTURAS

Aunque dedicamos una buena cantidad de pá ginas de este libro a hablar sobre la
eficiencia, el objetivo no es convertirlo en un experto en el diseñ o de programas
eficientes. Hay muchos libros largos (e incluso algunos buenos libros largos)
dedicados exclusivamente a ese tema.46 En el Capítulo 9, presentamos algunos de
los conceptos bá sicos que subyacen al aná lisis de complejidad. En este capítulo
usamos esos conceptos para observar la complejidad de algunos algoritmos
clá sicos. El objetivo de este capítulo es ayudarlo a desarrollar algunas intuiciones
generales sobre có mo abordar las cuestiones de eficiencia. Para cuando termine
este capítulo, debería comprender por qué algunos programas se completan en un
abrir y cerrar de ojos, por qué algunos necesitan ejecutarse durante la noche y por
qué algunos no se completará n en su vida.

Los primeros algoritmos que vimos en este libro se basaron en la enumeració n


exhaustiva de fuerza bruta. Argumentamos que las computadoras modernas
son tan rá pidas que a menudo se da el caso de que emplear algoritmos
inteligentes es una pé rdida de tiempo. Programa algo que sea simple y
obviamente correcto, y dé jalo funcionar.

Luego analizamos algunos problemas (p. ej., encontrar una aproximació n a las
raíces de un polinomio) donde el espacio de bú squeda era demasiado grande
para que la fuerza bruta fuera prá ctica. Esto nos llevó a considerar algoritmos
má s eficientes como la bú squeda de bisecció n y Newton-Raphson. El punto
principal fue que la clave de la eficiencia es un buen algoritmo, no trucos de
codificació n inteligentes.

En las ciencias (físicas, de la vida y sociales), los programadores a menudo


comienzan codificando rá pidamente un algoritmo simple para probar la
plausibilidad de una hipó tesis sobre un conjunto de datos y luego lo ejecutan en una
pequeñ a cantidad de datos. Si esto produce resultados alentadores, comienza el
arduo trabajo de producir una implementació n que se pueda ejecutar (quizá s una y
otra vez) en grandes conjuntos de datos. Dichas implementaciones deben basarse en
algoritmos eficientes.

Los algoritmos eficientes son difíciles de inventar. Los científicos informá ticos
profesionales exitosos podrían inventar tal vez un algoritmo durante toda su
carrera, si tienen suerte. La mayoría de nosotros nunca inventamos un algoritmo
novedoso. Lo que hacemos en cambio es aprender a reducir los aspectos má s
complejos de los problemas a los que nos enfrentamos a problemas previamente
resueltos. Má s específicamente, nosotros

 Desarrollar una comprensió n de la complejidad inherente del problema al


que nos enfrentamos,
 Piensa en có mo dividir ese problema en subproblemas, y
 Relacione esos subproblemas con otros problemas para los cuales
ya existen algoritmos eficientes.

46Introduccióna los Algoritmos, de Cormen, Leiserson, Rivest y Stein, es una excelente


fuente para aquellos de ustedes que no se sienten intimidados por una buena cantidad de
matemá ticas.
1 Capítulo 10. Algunos algoritmos simples y estructuras

Este capítulo contiene algunos ejemplos destinados a brindarle cierta intuició n sobre el
diseñ o de algoritmos. Muchos otros algoritmos aparecen en otras partes del libro.

Tenga en cuenta que el algoritmo má s eficiente no siempre es el algoritmo elegido.


Un programa que hace todo de la manera má s eficiente posible a menudo es
innecesariamente difícil de entender. A menudo, es una buena estrategia
comenzar resolviendo el problema en cuestió n de la manera má s directa posible,
instrumentarlo para encontrar cuellos de botella computacionales y luego buscar
formas de mejorar la complejidad computacional de aquellas partes del programa
que contribuyen a los cuellos de botella.

10.1 Algoritmos de búsqueda


Un algoritmo de bú squeda es un mé todo para encontrar un elemento o grupo de
elementos con propiedades específicas dentro de una colecció n de elementos. Nos
referimos a la colecció n de elementos como un espacio de bú squeda. El espacio de
bú squeda puede ser algo concreto, como un conjunto de registros mé dicos
electró nicos, o algo abstracto, como el conjunto de todos los nú meros enteros. Una
gran cantidad de problemas que ocurren en la prá ctica pueden formularse como
problemas de bú squeda.

Muchos de los algoritmos presentados anteriormente en este libro pueden verse


como algoritmos de bú squeda. En el Capítulo 3, formulamos la bú squeda de una
aproximació n a las raíces de un polinomio como un problema de bú squeda y
analizamos tres algoritmos (enumeració n exhaustiva, bú squeda de bisecció n y
Newton-Raphson) para buscar en el espacio de posibles respuestas.

En esta secció n, examinaremos dos algoritmos para buscar en una lista. Cada uno
cumple con las especificaciones
def search(L, e): """Asume
que L es una lista.
Devuelve True si e está en L y False en caso contrario"""

El lector astuto podría preguntarse si esto no es semá nticamente equivalente a la


expresió n de Python e en L. La respuesta es sí, lo es. Y si a uno no le preocupa la
eficiencia de descubrir si e está en L, simplemente debe escribir esa expresió n.

10.1.1 Búsqueda lineal y uso de la indirección para acceder a elementos


Python usa el siguiente algoritmo para determinar si un elemento está en una lista:
búsqueda de definición (L, e):
for i in range(len(L)):
if L[i] == e:
volver
Verdadero volver
Falso

Si el elemento e no está en la lista, el algoritmo realizará pruebas O(len(L)), es decir, la


complejidad es, en el mejor de los casos, lineal en la longitud de L. ¿Por qué “en el mejor
de los casos” lineal? Será lineal solo si cada operació n dentro del ciclo se puede realizar
en tiempo constante. Eso plantea la cuestió n de si Python recupera el i-é simo elemento
de una lista en tiempo constante. Dado que nuestro modelo de cá lculo supone que
obtener el
Capítulo 10. Algunos algoritmos simples y estructuras 1

contenido de una direcció n es una operació n de tiempo constante, la pregunta es


si podemos calcular la direcció n del i-é simo elemento de una lista en tiempo
constante.

Comencemos considerando el caso simple donde cada elemento de la lista es un


nú mero entero. Esto implica que cada elemento de la lista tiene el mismo
tamañ o, por ejemplo, cuatro unidades de memoria (cuatro bytes de ocho
bits47). En este caso, la direcció n en memoria del i-é simo elemento de la lista es
simplemente inicio + 4i, donde inicio es la direcció n del inicio de la lista. Por lo
tanto, podemos suponer que Python podría calcular la direcció n del i-é simo
elemento de una lista de enteros en tiempo constante.

Por supuesto, sabemos que las listas de Python pueden contener objetos de tipos
distintos a int, y que la misma lista puede contener objetos de muchos tipos y
tamañ os diferentes. Podría pensar que esto presentaría un problema, pero no es
así.

En Python, una lista se representa como una longitud (el nú mero de objetos en la
lista) y una secuencia de punteros48 de tamañ o fijo a los objetos. La figura 10.1
ilustra el uso de estos punteros. La regió n sombreada representa una lista que
contiene cuatro elementos.
El cuadro sombreado má s a la izquierda contiene un puntero a un nú mero entero
que indica la longitud de la lista. Cada uno de los otros cuadros sombreados contiene
un puntero a un objeto de la lista.

Figura 10.1 Listas de implementación

Si el campo de longitud tiene cuatro unidades de memoria y cada puntero


(direcció n) ocupa cuatro unidades de memoria, la direcció n del i-é simo elemento
de la lista se almacena en la direcció n inicio + 4 + 4i. Nuevamente, esta direcció n
se puede encontrar en tiempo constante, y luego el valor almacenado en esa
direcció n se puede usar para acceder al i-é simo elemento. Este acceso también es
una operació n de tiempo constante.

Este ejemplo ilustra una de las té cnicas de implementació n má s importantes que se


utilizan en la informá tica: la indirecció n.49 En términos generales, la indirecció n
implica acceder a algo accediendo primero a otra cosa que contiene una referencia.

47El
nú mero de bits que se utilizan para almacenar un nú mero entero, a menudo llamado
tamañ o de palabra, generalmente lo dicta el hardware de la computadora.
48De tamañ o 32 bits en algunas implementaciones y 64 bits en otras.
49Mi diccionario define "indirecció n" como "falta de franqueza y apertura: engañ o". De hecho,
la palabra generalmente tuvo una implicació n peyorativa hasta alrededor de 1950, cuando los
informá ticos se dieron cuenta de que era la solució n a muchos problemas.
1 Capítulo 10. Algunos algoritmos simples y estructuras

a la cosa inicialmente buscada. Esto es lo que sucede cada vez que usamos una
variable para referirnos al objeto al que está vinculada esa variable. Cuando
usamos una variable para acceder a una lista y luego una referencia almacenada
en esa lista para acceder a otro objeto, estamos pasando por dos niveles de
direccionamiento indirecto.50

10.1.2 Búsqueda binaria y explotación de suposiciones


Volviendo al problema de implementar search(L, e), ¿es O(len(L)) lo mejor que
podemos hacer? Sí, si no sabemos nada sobre la relació n de los valores de los
elementos de la lista y el orden en que se almacenan. En el peor de los casos,
tenemos que mirar cada elemento en L para determinar si L contiene e.

Pero supongamos que sabemos algo sobre el orden en que se almacenan los
elementos, por ejemplo, supongamos que sabemos que tenemos una lista de
nú meros enteros almacenados en orden ascendente. Podríamos cambiar la
implementació n para que la bú squeda se detenga cuando llegue a un nú mero mayor
que el nú mero que está buscando:
búsqueda de definición (L, e):
"""Asume que L es una lista, cuyos elementos están en orden
ascendente.
Devuelve True si e está en L y False de lo
contrario """ para i en range(len(L)):
si L[i] == e:
devuelve
True si L[i] >
e:
volver Falso
volver Falso

Esto mejoraría el tiempo promedio de funcionamiento. Sin embargo, no cambiaría la


complejidad del algoritmo en el peor de los casos, ya que en el peor de los casos se
examina cada elemento de L.

Sin embargo, podemos obtener una mejora considerable en la complejidad del peor
de los casos mediante el uso de un algoritmo, la bú squeda binaria, que es similar al
algoritmo de bú squeda de bisecció n utilizado en el Capítulo 3 para encontrar una
aproximació n a la raíz cuadrada de un nú mero de punto flotante. Allí confiamos en
el hecho de que existe un orden total intrínseco en los nú meros de punto flotante.
Aquí nos basamos en la suposició n de que la lista está ordenada.

La idea es sencilla:

1. Elija un índice,i, que divide la listaLaproximadamente a la mitad.


2. Pregunta siL[i] == mi.
3. Si no, pregunte siL[yo]es má s grande o má s pequeñ o quemi.
4. Dependiendo de la respuesta, busque en la mitad izquierda o derecha de Lparami.

50A menudo se ha dicho que “cualquier problema en computació n puede resolverse


agregando otro nivel de direccionamiento indirecto”. Siguiendo tres niveles de indirecció n,
atribuimos esta observació n a David J. Wheeler. El artículo "Autenticació n en sistemas
distribuidos: teoría y prá ctica", de Butler Lampson et al., contiene la observació n. Tambié n
contiene una nota al pie que dice que “Roger Needham atribuye esta observació n a David
Wheeler de la Universidad de Cambridge”.
Capítulo 10. Algunos algoritmos simples y estructuras 1

Dada la estructura de este algoritmo, no sorprende que la implementació n má s


directa de la bú squeda binaria utilice la recursividad, como se muestra en la figura
10.2.

Figura 10.2 Búsqueda binaria recursiva


búsqueda de definición (L, e):
"""Asume
La funció n externaque
de Lla es una10.2,
figura lista, cuyose),
buscar(L, elementos están en
tiene los mismos argumentos que la
orden ascendente.
funció n especificada
Devuelve anteriormente,
True si e estápero en una
L y especificació
False en cason ligeramente diferente.
contrario"""
La especificació n dice que la implementació n puede asumir que L está ordenado en
def bBuscar(L, e, bajo,
orden ascendente.
alto): #Decrementos
La carga de asegurarse de que se cumpla esta suposició n recae en la persona que
alto - bajo
llama a la búsi
squeda.
alto Si==elbajo:
supuesto no se cumple, la implementació n no tiene la
obligació n de comportarse L[bajo]
devuelve bien. Podría funcionar, pero tambié n podría bloquearse o
== e
devolver una respuesta incorrecta. ¿Debería modificarse la bú squeda para
medio = (bajo +
comprobar que se cumple
alto)//2 si el supuesto? Esto podría eliminar una fuente de errores,
pero anularía el propó ==
L[medio] sitoe:
de usar la bú squeda binaria, ya que verificar la suposició n
tomaría tiempo O volver
(len (L)).
Verdadero elif
Las funciones como la >búe:
L[medio] squeda a menudo se denominan funciones contenedoras.
if low una
La funció n proporciona == mid:
buena#no queda
interfaz nada
para por
el có digo del cliente, pero es
buscar return False
esencialmente un paso que no realiza cá lculos serios. En su lugar, llama a la funció n
demás:
auxiliar bSearch con los argumentos
volver apropiados.
bSearch(L, e, bajo,Esto plantea
medio - 1)la pregunta de por
demás:
qué no eliminar la bú squeda y hacer que los clientes llamen directamente a
return bBuscar(L, e, medio + 1, alto)
bSearch. La razó n es que los pará metros bajo y alto no tienen nada que ver con la
abstracció n de buscar un elemento en una lista. Son detalles de implementació n
que deben ocultarse a los programas de escritura que llaman a la bú squeda.

Analicemos ahora la complejidad de bSearch. Mostramos en la ú ltima secció n que


el acceso a la lista lleva un tiempo constante. Por lo tanto, podemos ver que
excluyendo la llamada recursiva, cada instancia de bSearch es O(1). Por lo tanto,
la complejidad de bSearch depende ú nicamente del nú mero de llamadas
recursivas.
1 Capítulo 10. Algunos algoritmos simples y estructuras

Si este fuera un libro sobre algoritmos, ahora nos sumergiríamos en un aná lisis
cuidadoso usando algo llamado relació n de recurrencia. Pero como no lo es,
adoptaremos un enfoque mucho menos formal que comienza con la pregunta
"¿Có mo sabemos que el programa termina?" Recuerde que en el Capítulo 3 hicimos
la misma pregunta sobre un ciclo while. Respondimos a la pregunta
proporcionando una funció n decreciente para el ciclo. Hacemos lo mismo aquí. En
este contexto, la funció n decreciente tiene las propiedades:

1. Asigna los valores a los que está n vinculados los pará metros
formales a un nú mero entero no negativo.
2. cuando su valor es0, la recursividad termina.
3. Para cada llamada recursiva, el valor de la funció n decreciente es menor
que el valor de la funció n decreciente al ingresar a la instancia de la
funció n que realiza la llamada.
La funció n decreciente para bSearch es alta-baja. La instrucció n if en la bú squeda
asegura que el valor de esta funció n decreciente sea al menos 0 la primera vez que se
llama a bSearch (propiedad de funció n decreciente 1).

Cuando se ingresa bSearch, si high-low es exactamente 0, la funció n no realiza una


llamada recursiva, simplemente devuelve el valor L[low] == e (satisfaciendo la
propiedad 2 de la funció n decreciente).

La funció n bSearch contiene dos llamadas recursivas. Una llamada usa argumentos
que cubren todos los elementos a la izquierda del medio, y la otra llamada usa
argumentos que cubren todos los elementos a la derecha del medio. En cualquier
caso, el valor de alto-bajo se reduce a la mitad (satisfaciendo la propiedad 3 de la
funció n decreciente).

Ahora entendemos por qué termina la recursividad. La siguiente pregunta es


¿cuá ntas veces se puede reducir a la mitad el valor de alto-bajo antes de alto-bajo ==
0?
Recalqueyoque logy(x) es el nú mero de veces que y tiene que ser multiplicado por sí mismo
para llegar a x. Por el contrario, si x se divide por y logy(x) veces, el resultado es 1. Esto implica
que el má ximo y el mínimo se pueden reducir a la mitad como mucho log2 (alto-mínimo) veces
antes de llegar a 0.

Finalmente, podemos responder a la pregunta, ¿cuá l es la complejidad algorítmica


de la bú squeda binaria? Desde cuando buscarllamadasbBuscarEl valor dealto–
bajoes igual alargo(L)-1, la complejidad debuscaresO(registro(largo(L))).51

Ejercicio de dedos:¿Por qué el có digo usamedio+1en vez demedioen la segunda


llamada recursiva?

51Recuerda que cuando miras ó rdenes de crecimiento, la base del logaritmo es irrelevante.
Capítulo 10. Algunos algoritmos simples y estructuras 1

10.2 Algoritmos de clasificación


Acabamos de ver que si sabemos que una lista está ordenada, podemos explotar esa
informació n para reducir en gran medida el tiempo necesario para buscar en una
lista. ¿Significa esto que cuando se le pide que busque en una lista, primero debe
ordenarla y luego realizar la bú squeda?

Sea O(sortComplexity(L)) la complejidad de ordenar una lista. Dado que sabemos que
siempre podemos buscar una lista en tiempo O(len(L)), la cuestió n de si primero
debemos ordenar y luego buscar se reduce a la pregunta, es (sortComplexity(L) +
log(len(L) )) < largo(L)? La respuesta, lamentablemente, es no. No se puede ordenar
una lista sin mirar cada elemento de la lista al menos una vez, por lo que no es posible
ordenar una lista en tiempo sublineal.

¿Significa esto que la bú squeda binaria es una curiosidad intelectual sin importancia
prá ctica? Felizmente, no. Supongamos que uno espera buscar en la misma lista
muchas veces. Bien podría tener sentido pagar los gastos generales de clasificar la
lista una vez y luego amortizar el costo de la clasificació n en muchas bú squedas. Si
esperamos buscar en la lista k veces, la pregunta relevante es: ¿es
(sortComplexity(L) + k*log(len(L))) menor que k*len(L)? A medida que k se vuelve
grande, el tiempo requerido para ordenar la lista se vuelve cada vez má s irrelevante.

El tamañ o de k debe ser depende del tiempo que se tarde en ordenar una lista. Si,
por ejemplo, la clasificació n fuera exponencial en el tamañ o de la lista, k tendría que
ser bastante grande.

Afortunadamente, la clasificació n se puede hacer de manera bastante eficiente. Por


ejemplo, la implementació n está ndar de clasificació n en la mayoría de las
implementaciones de Python se ejecuta aproximadamente en el tiempo O(n*log(n)),
donde n es la longitud de la lista. En la prá ctica, rara vez necesitará implementar su
propia funció n de clasificació n. En la mayoría de los casos, lo correcto es utilizar el
mé todo de clasificació n integrado de Python (L.sort() clasifica la lista L) o su funció n
integrada sorted (sorted(L) devuelve una lista con los mismos elementos que L ,
pero no muta L). Presentamos algoritmos de clasificació n aquí principalmente para
proporcionar algo de prá ctica en el pensamiento sobre el diseñ o de algoritmos y el
aná lisis de complejidad.

Comenzamos con un algoritmo simple pero ineficiente, el ordenamiento por


selecció n. La ordenació n por selecció n, Figura 10.3, funciona manteniendo el bucle
invariable de modo que, dada una partició n de la lista en un prefijo (L[0:i]) y un
sufijo (L[i+1:len(L)]), el prefijo está ordenado y ningú n elemento del prefijo es
mayor que el elemento má s pequeñ o del sufijo.
1 Capítulo 10. Algunos algoritmos simples y estructuras

Usamos la inducció n para razonar acerca de las invariantes de bucle.

 Caso base: Al comienzo de la primera iteració n, el prefijo está vacío, es


decir, el sufijo es la lista completa. El invariante es (trivialmente) cierto.
 Paso de inducció n: en cada paso del algoritmo, movemos un elemento del
sufijo al prefijo. Hacemos esto agregando un elemento mínimo del sufijo al
final del prefijo. Debido a que el invariante se mantuvo antes de que
moviéramos el elemento, sabemos que después de agregar el elemento, el
prefijo aú n está ordenado. Tambié n sabemos que como eliminamos el
elemento má s pequeñ o del sufijo, ningú n elemento del prefijo es má s grande
que el elemento má s pequeñ o del sufijo.
 Cuando se sale del bucle, el prefijo incluye la lista completa y el sufijo está
vacío. Por lo tanto, la lista completa ahora está ordenada en orden
ascendente.

def selOrdenar(L): Figura 10.3 Clasificación por selección


"""Asume que L es una lista de elementos que se
pueden
Es difícil imaginar uncomparar
algoritmousando >.
de clasificació n má s simple o má s obviamente
Ordena L en orden
correcto. Desafortunadamente, es bastante ineficiente.52 La complejidad del ciclo
ascendente""" sufijoInicio = 0
internowhile
es O(len(L)). La complejidad
sufijoStart != len(L): del bucle exterior tambié n es O(len(L)).
Entonces, la#mira
complejidad de latoda la
cada elemento enfunció n es O(len(L)2). Es decir, es cuadrá tico en la
el sufijo
longitud de L.for i in range(suffixStart,
len(L)): if L[i] <
10.2.1 Ordenar por fusión
L[suffixStart]:
#swap posición de elementos
L[sufijoInicio],
Afortunadamente, podemos hacerlo mucho L[i] = L[i],
mejor que elL[sufijoInicio]
tiempo cuadrá tico usando
un algoritmo de divide y vencerá s. La idea bá sica es combinar soluciones de
instancias má s simples del problema original. En general, un algoritmo divide y
vencerá s se caracteriza por

1. Un tamañ o de entrada de umbral, por debajo del cual el problema no se


subdivide,
2. El tamañ o y la cantidad de subinstancias en las que se divide una
instancia, y
3. El algoritmo utilizado para combinar subsoluciones.

El umbral a veces se denomina base recursiva. Para el ítem 2, es habitual


considerar la relació n entre el tamañ o del problema inicial y el tamañ o de la
subinstancia. En la mayoría de los ejemplos que hemos visto hasta ahora, la
proporció n era 2.

52Pero no el má s ineficiente de los algoritmos de clasificació n, como sugirió un candidato


exitoso a la presidencia de los EE. UU. Verhttp://www.youtube.com/watch?
v=k4RRi_ntQc8.
Capítulo 10. Algunos algoritmos simples y estructuras 1

Ordenar por fusiónes un algoritmo prototípico de divide y vencerá s. Fue


inventado en 1945 por John von Neumann y todavía se usa ampliamente. Como
muchos algoritmos de divide y vencerá s, es má s fá cil describirlo recursivamente.

1. Si la lista es larga0o1, ya está ordenado.


2. Si la lista tiene má s de un elemento, divídala en dos listas y use la
ordenació n por combinació n para ordenar cada una de ellas.
3. Combinar los resultados.

La observació n clave hecha por von Neumann es que dos listas ordenadas pueden
fusionarse eficientemente en una sola lista ordenada. La idea es mirar el primer
elemento de cada lista y mover el má s pequeñ o de los dos al final de la lista de
resultados. Cuando una de las listas está vacía, todo lo que queda es copiar los
elementos restantes de la otra lista. Considere, por ejemplo, fusionar las dos listas
[1,5,12,18,19,20] y [2,3,4,17]:

A la izquierda en la lista1Izquierda en lista2Resultado


[1,5,12,18,19,20] [2,3,4,17] []
[5,12,18,19,20] [2,3,4,17] [1]
[5,12,18,19,20] [3,4,17] [1,2]
[5,12,18,19,20] [4,17] [1,2,3]
[5,12,18,19,20] [17] [1,2,3,4]
[12,18,19,20] [17] [1,2,3,4,5]
[18,19,20] [17] [1,2,3,4,5,12]
[18,19,20] [] [1,2,3,4,5,12,17]
[] [] [1,2,3,4,5,12,17,18,19,20]

¿Cuá l es la complejidad del proceso de fusió n? Implica dos operaciones de tiempo


constante, comparando los valores de los elementos y copiando elementos de
una lista a otra. El nú mero de comparaciones es O(len(L)), donde L es la má s
larga de las dos listas. El nú mero de operaciones de copia es O(len(L1) + len(L2)),
porque cada elemento se copia exactamente una vez. Por lo tanto, fusionar dos
listas ordenadas es lineal en la longitud de las listas.

La figura 10.4 contiene una implementació n del algoritmo de clasificació n por


fusió n. Observe que hemos convertido al operador de comparació n en un pará metro
de la funció n mergeSort.
El valor predeterminado del pará metro es el operador lt definido en el mó dulo
está ndar de Python denominado operator. Este mó dulo define un conjunto de
funciones correspondientes a los operadores integrados de Python (por ejemplo, <
para nú meros). En la Secció n 10.2.2, explotaremos esta flexibilidad.
1 Capítulo 10. Algunos algoritmos simples y estructuras

def fusionar (izquierda, derecha, comparar):


"""Asume que izquierda y derecha son listas
ordenadas y comparar define un orden en los
elementos.
Devuelve una nueva lista ordenada (por comparación) que
contiene los mismos elementos que contendría
(izquierda + derecha)."""

resultado
= [] i, j
= 0, 0
while i < len(izquierda) y j <
len(derecha): if compare(left[i],
right[j]):
resultado.append(izq
uierda[i]) i += 1
demás:
resultado.append(derec
ha[j]) j += 1
while (i <
len(izquierda)):
resultado.append(izq
uierda[i]) i += 1
while (j <
len(derecha)):resulta
do.append(derecha[j])
j += 1
operador de

importación de

resultados

devueltos

def mergeSort(L, comparar = operator.lt):

Figura 10.4 Clasificación por fusión

Analicemos la complejidad decombinarOrdenar. Ya sabemos que la complejidad


temporal deuniresO(largo(L)). En cada nivel de recursividad, el nú mero total de
elementos que se fusionará n eslargo(L). Por lo tanto, la complejidad temporal
decombinarOrdenaresO(largo(L))multiplicado por el nú mero de niveles de
recursividad. DesdecombinarOrdenardivide la lista por la mitad cada vez, sabemos
que el nú mero de niveles de recursió n esO(registro(largo(L)). Por lo tanto, la complejidad
temporal decombinarOrdenaresO(n*log(n)), dó ndenorteeslargo(L).

eleses mucho mejor que el tipo de selecció n O(len(L)2). Por ejemplo, si L tiene 10.000
mielementos, largo(L)2 escien millones perolargo(L)*log2(largo(L)) esacerca de130,000.

Esta mejora en la complejidad del tiempo tiene un precio. La clasificació n por


selecció n es un ejemplo de un algoritmo de clasificació n en el lugar. Debido a que
funciona intercambiando el lugar de los elementos dentro de la lista, utiliza solo una
cantidad constante de almacenamiento adicional (un elemento en nuestra
implementació n). Por el contrario, el algoritmo de clasificació n por fusió n
Capítulo 10. Algunos algoritmos simples y estructuras 1

implica hacer copias de la lista. Esto significa que su complejidad espacial es


O(largo(L)). Esto puede ser un problema para listas grandes. 53

10.2.2 Explotación de funciones como parámetros


Supongamos que queremos ordenar una lista de nombres escritos como firstName
lastName, por ejemplo, la lista['Chris Terman', 'Tom Brady', 'Eric
Grimson', 'Gisele Bündchen'].
La figura 10.5 define dos funciones de ordenació n y luego las usa para ordenar una
lista de dos maneras diferentes. Cada funció n importa la cadena del mó dulo
está ndar de Python y usa la funció n de divisió n de ese mó dulo. Los dos argumentos
para dividir son cadenas. El segundo argumento especifica un separador (un
espacio en blanco en el có digo de la figura 10.5) que se usa para dividir el primer
argumento en una secuencia de subcadenas. El segundo argumento es opcional. Si
se omite ese argumento, la primera cadena se divide utilizando cadenas arbitrarias
de caracteres de espacio en blanco (espacio, tabulador, nueva línea, retorno y salto
de pá gina).
def lastNameFirstName(nombre1,
nombre2):
Figura 10.5cadena
Ordenando de
una lista de nombres
importación
nombre1 =
cadena.split(nombre1, ' ')
nombre2 =
cadena.split(nombre2, ' ') if
nombre1[1] != nombre2[1]:
devolver nombre1[1] < nombre2[1]
53Ordenación rápida, inventado por CAR Hoare en 1960, es conceptualmente similar a
else: #apellidos iguales, ordenar por nombre
merge sort, pero considerablemente má s complejo. Tiene la ventaja de que solo necesita
return nombre1[0] < nombre2[0]
log(n) espacio adicional. A diferencia de la ordenació n por combinació n, su tiempo de
ejecució n depende de la forma en que los elementos de la lista que se van a ordenar está n
def firstNameLastName(name1,
ordenados entre sí. Aunque su tiempo de ejecució n en el peor de los casos es
name2): cadena de importación
O(n2),su tiempo
nombre1de ejecució n esperado es solo O (n * log
= (n)).
cadena.split(nombre1, ' ')
nombre2 =
cadena.split(nombre2, ' ') if
nombre1[0] != nombre2[0]:
devolver nombre1[0] < nombre2[0]
else: #primeros nombres iguales, ordenar por
apellido return nombre1[1] < nombre2[1]
1 Capítulo 10. Algunos algoritmos simples y estructuras

10.2.3 Ordenando en Python


El algoritmo de clasificació n utilizado en la mayoría de las implementaciones de
Python se llama timsort.54 La idea clave es aprovechar el hecho de que en muchos
conjuntos de datos los datos ya está n parcialmente ordenados. El rendimiento de
Timsort en el peor de los casos es el mismo que el de merge sort, pero en promedio
funciona considerablemente mejor.

Como se mencionó anteriormente, el mé todo list.sort de Python toma una lista


como su primer argumento y la modifica. Por el contrario, la funció n de Python
sorted toma un objeto iterable (por ejemplo, una lista o un diccionario) como
primer argumento y devuelve una nueva lista ordenada. Por ejemplo, el có digo
L = [3,5,2]
D = {'a':12, 'c':5, 'b':'perro'}
imprimir
ordenado(L)
imprimir L
L.ordenar()
imprimir L
imprimir
ordenado(D)
D.sort()

imprimirá

[2, 3, 5]
[3, 5, 2]
[2, 3, 5] ['a',
'b', 'c']
Rastreo (llamadas recientes más última):
Archivo "/current/mit/Teaching/600/book/10-
AlgorithmsChapter/algorithms.py", línea 168, en <módulo>
D.ordenar()
AttributeError: el objeto 'dict' no tiene atributo 'sort'

Observe que cuando la funció n ordenada se aplica a un diccionario, devuelve una


lista ordenada de las claves del diccionario. Por el contrario, cuando el método sort
se aplica a un diccionario, provoca que se genere una excepció n ya que no existe el
mé todo dict.sort.

Tanto el método list.sort como la funció n sorted pueden tener dos pará metros
adicionales. El pará metro clave juega el mismo papel que comparar en nuestra
implementació n de clasificació n por fusió n: se utiliza para proporcionar la funció n de
comparació n que se utilizará . El pará metro inverso especifica si la lista debe ordenarse
en orden ascendente o descendente. Por ejemplo, el có digo

L = [[1,2,3], (3,2,1,0), 'abc']


imprimir ordenado (L, clave = len, inversa = Verdadero)

ordena los elementos de L en orden inverso de longitud e imprime


[(3, 2, 1, 0), [1, 2, 3], 'abc']

54Timsort fue inventado por Tim Peters en 2002 porque no estaba satisfecho con el
algoritmo anterior utilizado en Python.
Capítulo 10. Algunos algoritmos simples y estructuras 1

Tanto el método list.sort como la funció n sorted proporcionan clasificaciones estables.


Esto significa que si dos elementos son iguales con respecto a la comparació n utilizada
en la ordenació n, su ordenació n relativa en la lista original (u otro objeto iterable) se
conserva en la lista final.

10.3 tablas hash


Si juntamos la ordenació n por combinació n con la bú squeda binaria, tenemos una
buena manera de buscar listas. Usamos la ordenació n por combinació n para
preprocesar la lista en el tiempo O(n*log(n)), y luego usamos la bú squeda binaria
para probar si los elementos está n en la lista en el tiempo O(log(n)). Si buscamos en
la lista k veces, la complejidad temporal total es O(n*log(n) + k*log(n)).

Esto es bueno, pero aú n podemos preguntar, ¿es logarítmico lo mejor que podemos
hacer para la bú squeda cuando estamos dispuestos a hacer algú n
preprocesamiento?

Cuando introdujimos el tipo dict en el Capítulo 5, dijimos que los diccionarios usan
una técnica llamada hashing para hacer la bú squeda en el tiempo que es casi
independiente del tamañ o del diccionario. La idea bá sica detrá s de una tabla hash es
simple. Convertimos la clave en un nú mero entero y luego usamos ese nú mero
entero para indexar en una lista, lo que se puede hacer en tiempo constante. En
principio, los valores de cualquier tipo inmutable se pueden convertir fá cilmente en
un nú mero entero. Despué s de todo, sabemos que la representació n interna de cada
objeto es una secuencia de bits, y cualquier secuencia de bits puede verse como la
representació n de un nú mero entero. Por ejemplo, la representació n interna de 'abc'
es la cadena de bits 011000010110001001100011, que puede verse como una
representació n del entero decimal 6,382,179. Por supuesto,

¿Qué pasa con las situaciones en las que las claves ya son nú meros enteros?
Imagine, por el momento, que estamos implementando un diccionario cuyas
claves son nú meros de la Seguridad Social de EE. UU.55 Si representá ramos el
diccionario mediante una lista con 109 elementos y usá ramos los nú meros de la
Seguridad Social para indexar la lista, podríamos hacer bú squedas en tiempo
constante Por supuesto, si el diccionario contuviera entradas de só lo diez
mil(104) personas, esto desperdiciaría bastante espacio.

Lo que nos lleva al tema de las funciones hash. Una funció n hash asigna un gran
espacio de entradas (p. ej., todos los nú meros naturales) a un espacio má s
pequeñ o de salidas (p. ej., los nú meros naturales entre 0 y 5000). Las funciones
hash se pueden usar para convertir un gran espacio de claves en un espacio má s
pequeñ o de índices enteros.

Dado que el espacio de posibles salidas es má s pequeñ o que el espacio de


posibles entradas, una funció n hash es un mapeo de muchos a uno, es decir, se
pueden mapear mú ltiples entradas diferentes a la misma salida. Cuando dos
entradas se asignan a la misma salida, se denomina colisió n, un tema al que
volveremos en breve. Una buena funció n hash produce una distribució n
uniforme, es decir, cada resultado en el rango es igualmente probable, lo que
minimiza la probabilidad de colisiones.

55Un nú mero de Seguro Social de los Estados Unidos es un nú mero entero de nueve dígitos.
1 Capítulo 10. Algunos algoritmos simples y estructuras

Diseñ ar buenas funciones hash es sorprendentemente desafiante. El problema es


que uno quiere que las salidas se distribuyan uniformemente dada la distribució n
esperada de entradas. Supongamos, por ejemplo, que uno cifra los apellidos
realizando algú n cá lculo en las tres primeras letras. En los Países Bajos, donde
aproximadamente el 5 % de los apellidos comienzan con “van” y otro 5 % con “de”,
la distribució n dista mucho de ser uniforme.

La figura 10.6 usa una funció n hash simple (recuerde que i%j devuelve el resto
cuando el entero i se divide por el entero j) para implementar un diccionario con
enteros como claves.

La idea bá sica es representar una instancia de la clase intDict mediante una lista de
cubos hash, donde cada cubo es una lista de pares clave/valor. Al hacer que cada
cubo sea una lista, manejamos las colisiones almacenando todos los valores que
generan hash en el mismo cubo en la lista.

La tabla hash funciona de la siguiente manera: la variable de instancia cubos se


inicializa en una lista de numBuckets listas vacías. Para almacenar o buscar una
entrada con la clave dictKey, usamos la funció n hash % para convertir dictKey en un
nú mero entero, y usamos ese nú mero entero para indexar en cubos para encontrar el
cubo hash asociado con dictKey. Luego buscamos ese cubo (que es una lista)
linealmente para ver si hay una entrada con la clave dictKey. Si estamos haciendo
una bú squeda y hay una entrada con la clave, simplemente devolvemos el valor
almacenado con esa clave. Si no hay ninguna entrada con esa clave, devolvemos
Ninguno. Si se va a almacenar un valor, reemplazamos el valor en la entrada
existente, si se encontró una, o agregamos una nueva entrada al depó sito si no se
encontró ninguna.

Hay muchas otras formas de manejar las colisiones, algunas considerablemente má s


eficientes que usar listas. Pero este es probablemente el mecanismo má s simple y
funciona bien si la tabla hash es lo suficientemente grande y la funció n hash
proporciona una buena aproximació n a una distribució n uniforme.

Tenga en cuenta que el mé todo str produce una representació n de un diccionario


que no está relacionado con el orden en que se agregaron los elementos, sino que
está ordenado por los valores a los que las claves se convierten en hash. Esto explica
por qué no podemos predecir el orden de las teclas en un objeto de tipo dict.
Capítulo 10. Algunos algoritmos simples y estructuras 1

clase intDict(objeto):
"""Un diccionario con claves enteras"""

def __init__(self, numBuckets):


"""Crear un diccionario
vacío""" self.buckets = []
self.numBuckets = numBuckets
for i in range(numBuckets):
self.depósitos.append([])

def addEntry(self, dictKey, dictVal):


"""Supone que dictKey es un int. Agrega una
entrada.""" hashBucket = self.buckets[dictKey
%self.numBuckets] for i in range(len(hashBucket)):
if hashBucket[i][0] == dictKey:
hashBucket[i] = (dictKey,
dictVal) return
hashBucket.append((dictKey, dictVal))

def obtenerValor(self, dictKey):


"""Asume dictKey un int. Devuelve la entrada
asociada con la clave dictKey"""
hashBucket = self.cubos[dictKey%self.numBuckets]
para e en hashBucket:
if e[0] ==
dictKey:
devuelve e[1]
volver Ninguno

def
__str__(auto
): resultado
= '{'
para b en

Figura 10.6 Implementando diccionarios usando hash

El siguiente có digo primero construye un intDict con veinte entradas. Los valores de las
entradas son los nú meros enteros del 0 al 19. Las claves se eligen al azar de nú meros
enteros en el rango de 0 a 105 - 1. (Discutimos el mó dulo aleatorio en el Capítulo 12). El
có digo luego imprime el intDict usando el método str definido en la clase. Finalmente,
imprime los cubos de hash individuales iterando sobre D.cubos. (Esta es una terrible
violació n de la ocultació n de informació n, pero pedagó gicamente ú til).
importar al azar #un módulo de biblioteca estándar

D = intDict(29)
para i en el rango (20):
#elegir un int aleatorio entre 0 y 10**5
clave = random.randint(0, 10**5)
D.addEntry(clave, i)
imprime 'El valor del intDict es:'
imprime D
print '\n', 'Los cubos son:'
para hashBucket en D.buckets: #violates abstracción barrera
print ' ', hashBucket
1 Capítulo 10. Algunos algoritmos simples y estructuras

Cuando ejecutamos este có digo imprimió 56


El valor de intDict es:
{93467:5,78736:19,90718:4,529:16,12130:1,7173:7,68075:10,15851:0,
47027:14,45288:8,5819:17,83076:6,55236:13,19481:9,11854:12,29604:11,
45902:15,14408:18,24965:3,89377:2}

los baldes son:


[(93467, 5)]
[(78736, 19)]
[]
[]
[]
[]
[(90718, 4)]
[(529, 16)]
[(12130, 1)]
[]
[(7173, 7)]
[]
[(68075, 10)]
[]
[]
[]
[]
[(15851, 0)]
[(47027, 14)]
[(45288, 8), (5819, 17)]
[(83076, 6), (55236, 13)]
[]
[(19481, 9), (11854, 12)]
[]
[(29604, 11), (45902, 15), (14408, 18)]
[(24965, 3)]
[]
[]
[(89377, 2)]

Cuando violamos la barrera de la abstracció n y echamos un vistazo a la


representació n de intDict, vemos que muchos de los cubos de hash está n vacíos.
Otros contienen una, dos o tres tuplas, dependiendo del nú mero de colisiones que
hayan ocurrido.

¿Cuá l es la complejidad de getValue? Si no hubiera colisiones, sería O(1), porque


cada depó sito de hash tendría una longitud de 0 o 1. Pero, por supuesto, podría
haber colisiones. Si todo tuviera un hash en el mismo cubo, sería O(n) donde n es el
nú mero de entradas en el diccionario, porque el có digo realizaría una bú squeda
lineal en ese cubo hash. Al hacer que la tabla hash sea lo suficientemente grande,
podemos reducir el nú mero de colisiones lo suficiente como para permitirnos tratar
la complejidad como O(1). Es decir, podemos intercambiar espacio por tiempo. Pero,
¿cuá l es la compensació n? Para responder a esta pregunta, se necesita saber un
poquito de probabilidad, por lo que posponemos la respuesta para el Capítulo 12.

56Dado que los nú meros enteros se eligieron al azar, probablemente obtendrá resultados
diferentes si lo ejecuta.
11 PLOTEO Y MÁS SOBRE CLASES

A menudo, el texto es la mejor manera de comunicar informació n, pero a veces hay


mucho de cierto en el proverbio chino 圖片的意義可以表達近萬字 ("El significado de
una imagen puede expresar diez mil palabras"). Sin embargo, la mayoría de los
programas se basan en la salida de texto para comunicarse con sus usuarios. ¿Por qué?
Porque en muchos lenguajes de programació n presentar datos visuales es demasiado
difícil. Afortunadamente, es fá cil de hacer en Python.

11.1 Trazado usando PyLab


PyLab es un mó dulo de biblioteca está ndar de Python que proporciona muchas de las
funciones de MATLAB, "un lenguaje informá tico técnico de alto nivel y un entorno
interactivo para el desarrollo de algoritmos, la visualizació n de datos, el aná lisis de
datos y el cá lculo numérico". observe algunas de las funciones má s avanzadas de
PyLab, pero en este capítulo nos centraremos en algunas de sus funciones para trazar
datos. En el sitio web matplotlib.sourceforge.net/users/index.html se encuentra una
guía completa para el usuario de PyLab. También hay una serie de sitios web que
ofrecen excelentes tutoriales. No intentaremos proporcionar una guía del usuario o un
tutorial completo aquí. En su lugar, en este capítulo simplemente proporcionaremos
algunos grá ficos de ejemplo y explicaremos el có digo que los generó . Otros ejemplos
aparecen en capítulos posteriores.

Comencemos con un ejemplo simple que usa pylab.plot para producir dos grá ficos.
ejecutando
importar pylab

pylab.figure(1) #crear la figura 1


pylab.plot([1,2,3,4], [1,7,3,5]) #dibujar en la
figura 1 pylab.show() #mostrar la figura en
pantalla

hará que aparezca una ventana en el monitor de su computadora. Su apariencia


exacta puede depender del sistema operativo de su má quina, pero será similar a la
siguiente:

57http://www.mathworks.com/products/matlab/description1.html?s_cid=ML_b1008_desintro
1 Capítulo 11. Trazado y más sobre las

La barra en la parte superior contiene el nombre de la ventana, en este caso “Figura 1”.

La secció n central de la ventana contiene el grá fico generado por la invocació n de


pylab.plot. Los dos pará metros de pylab.plot deben ser secuencias de la misma
longitud. El primero especifica las coordenadas x de los puntos que se trazará n y el
segundo especifica las coordenadas y. Juntos, proporcionan una secuencia de cuatro
<x, y>pares de coordenadas,[(1,1), (2,7), (3,3), (4,5)].Estos se trazan en
orden. A medida que se traza cada punto, se dibuja una línea que lo conecta con el
punto anterior.

La ú ltima línea de có digo, pylab.show(), hace que aparezca la ventana en la pantalla de


la computadora.58 Si esa línea no estuviera presente, la figura aú n se habría
producido, pero no se habría mostrado. Esto no es tan tonto como parece a primera
vista, ya que uno bien podría optar por escribir una figura directamente en un
archivo, como haremos má s adelante, en lugar de mostrarla en la pantalla.

La barra en la parte inferior de la ventana contiene varios botones. El botó n má s a la


derecha se usa para escribir el grá fico en un archivo.59 El siguiente botó n a la
izquierda se usa para ajustar la apariencia del grá fico en la ventana. Los siguientes
cuatro botones se usan para panoramizar y hacer zoom. Y el botó n de la izquierda se
usa para restaurar la figura a su apariencia original después de que haya terminado de
jugar con la panorá mica y el zoom.

Es posible producir mú ltiples figuras y escribirlas en archivos. Estos archivos pueden


tener el nombre que desee, pero todos tendrá n la extensió n de archivo .png. La
extensió n de archivo .png indica que el archivo está en formato Portable Networks
Graphics. Este es un está ndar de dominio pú blico para representar imá genes.

58En algunos sistemas operativos, pylab.show() hace que el proceso que ejecuta Python se
suspenda hasta que se cierra la figura (haciendo clic en el botó n rojo redondo en la esquina
superior izquierda de la ventana). Esto es desafortunado. La solució n habitual es asegurarse de
que pylab.show() sea la ú ltima línea de có digo que se ejecutará .
59Para aquellos de ustedes que son demasiado jó venes para saberlo, el icono representa un
"disquete". Los disquetes fueron introducidos por primera vez por IBM en 1971. Tenían 8
pulgadas de diá metro y contenían 80,000 bytes. A diferencia de los disquetes posteriores, en
realidad eran disquetes. La PC IBM original tenía una ú nica unidad de disquete de 5,5 pulgadas y
160 Kbytes. Durante la mayor parte de las dé cadas de 1970 y 1980, los disquetes fueron el
dispositivo de almacenamiento principal para las computadoras personales. La transició n a los
gabinetes rígidos (como se representa en el ícono que inició esta digresió n) comenzó a mediados
de la dé cada de 1980 (con Macintosh), lo que no impidió que la gente siguiera llamá ndolos
disquetes.
Capítulo 11. Trazado y más sobre las 1

El có digo
pylab.figure(1) #crear la figura 1
pylab.plot([1,2,3,4], [1,2,3,4]) #dibujar en la
figura 1 pylab.figure(2) #crear la figura 2
pylab .plot([1,4,2,3], [5,6,7,8]) #dibujar en la
figura 2 pylab.savefig('Figura-Addie') #guardar la
figura 2 pylab.figure(1) #ir volver a trabajar en
la figura 1 pylab.plot([5,6,10,3]) #dibujar de
nuevo en la figura 1 pylab.savefig('Figura-Jane')
#guardar la figura 1

produce y guarda en archivos llamados Figura-Jane.pngyFigura-Addie.pnglas dos


parcelas de abajo.

Observe que la ú ltima llamada a pylab.plot se pasa solo un argumento. Este


argumento proporciona los valores de y. Los valores x correspondientes
predeterminados son range(len([5, 6, 10, 3])), razó n por la cual varían de 0 a 3 en

este caso.
contenido deFigure-Jane.pngContenido de Figura-Addie.png

PyLab tiene una noció n de "cifra actual". Ejecutar pylab.figure(x) establece la figura
actual en la figura numerada x. Las llamadas ejecutadas posteriormente a las funciones
de trazado se refieren implícitamente a esa figura hasta que se produce otra invocació n
de pylab.figure. Esto explica por qué la figura escrita en el archivo Figure-Addie.png fue
la segunda figura creada.

Veamos otro ejemplo. El có digo


capital = 10000 #tasa de interés de la
inversión inicial = 0,05
años = 20
valores =
[]
para i en rango (años +
1):valores.append(principal)
principal += principal*tasadeinterés
pylab.plot(valores)

produce la trama de la izquierda abajo.


1 Capítulo 11. Trazado y más sobre las

Si observamos el có digo, podemos deducir que se trata de un grá fico que muestra el
crecimiento de una inversió n inicial de $10 000 a una tasa de interés compuesta anual
del 5 %.
Sin embargo, esto no se puede inferir fá cilmente mirando solo la trama en sí. Eso es
algo malo. Todas las parcelas deben tener títulos informativos y todos los ejes deben
estar etiquetados.

Si añ adimos al final de nuestro có digo las líneas


pylab.title('Crecimiento del 5 %, capitalización
anual') pylab.xlabel('Años de capitalización')
pylab.ylabel('Valor del principal ($)')

obtenemos el grá fico de arriba y a la derecha.

Para cada curva trazada, hay un argumento


opcional que es una cadena de formato que
indica el color y el tipo de línea de la
grá fica.60 Las letras y los símbolos de la
cadena de formato se derivan de los
utilizados en MATLAB y se componen de un
indicador de color seguido por un indicador
de estilo de línea. La cadena de formato
predeterminada es 'b-', que produce una
línea azul só lida. Para trazar lo anterior con
círculos rojos, uno reemplazaría la llamada
pylab.plot(values) por pylab.plot(values,
'ro'), que
produce la trama de la derecha. Para obtener una lista completa de indicadores de
color y estilo de línea,
consultehttp://matplotlib.sourceforge.net/api/pyplot_api.html#matplotlib.pyplot.plo
t.

60Para mantener el precio bajo, decidimos publicar este libro en blanco y negro. Eso planteó un
dilema: ¿deberíamos discutir có mo usar el color en las tramas o no? Llegamos a la conclusió n de
que el color es demasiado importante para ignorarlo. Si desea ver có mo se ven los grá ficos en
color, ejecute el có digo.
Capítulo 11. Trazado y más sobre las 1

También es posible cambiar el tamañ o de letra y el ancho de línea utilizado en los


grá ficos. Esto se puede hacer usando argumentos de palabras clave en llamadas
individuales a funciones, por ejemplo, el có digo
capital = 10000 #tasa de interés de la
inversión inicial = 0,05
años = 20
valores =
[]
para i en rango (años +
1):valores.append(principal)
principal += principal*tasa de interés
pylab.plot(valores, ancho de línea = 30)
pylab.title('Crecimiento del 5 %, compuesto
anual',
tamaño de fuente = 'xx-grande')
pylab.xlabel('Años de capitalización', tamaño de fuente = 'x-pequeño')
pylab.ylabel('Valor del principal ($)')

produce la trama intencionalmente extrañ a

También es posible cambiar los valores predeterminados, que se conocen como


"configuració n rc". (El nombre "rc" se deriva de la extensió n de archivo .rc utilizada
para los archivos de configuració n de tiempo de ejecució n en Unix). Estos valores se
almacenan en una variable similar a un diccionario a la que se puede acceder a
través del nombre pylab.rcParams. Entonces, por ejemplo, puede establecer el ancho
de línea predeterminado en 6 puntos61 ejecutando el có digo
pylab.rcParams['lines.linewidth'] = 6.

61El punto es una medida utilizada en tipografía. Es igual a 1/72 de pulgada, que es
0,3527 mm.
1 Capítulo 11. Trazado y más sobre las

Los valores predeterminados utilizados en la mayoría de los ejemplos de este libro se


establecieron con el có digo
#establecer ancho de línea
pylab.rcParams['lines.linewidth'] = 4
#establecer tamaño de fuente para
títulos
pylab.rcParams['axes.titlesize'] = 20
#establecer tamaño de fuente para
etiquetas en ejes
pylab.rcParams['axes.labelsize '] = 20
#establecer el tamaño de los números en
el eje x
pylab.rcParams['xtick.labelsize'] = 16
#establecer el tamaño de los números =en7
pylab.rcParams['xtick.mayor.tamaño
el
'] eje y
#establecer el tamaño de las
pylab.rcParams['ytick.labelsize'] = 16
marcas en el
#establecer eleje y
tamaño de marcas en el
pylab.rcParams['ytick.mayor.tamaño = 7
eje
'] x

Si está viendo grá ficos en una pantalla a color, tendrá pocas razones para
personalizar esta configuració n. Personalizamos la configuració n que usamos
para que fuera má s fá cil leer los grá ficos cuando los redujimos y los convertimos a
blanco y negro. Para obtener una discusió n completa sobre có mo personalizar la
configuració n,
consultehttp://matplotlib.sourceforge.net/users/customizing.html.

11.2 Trazado de hipotecas, un ejemplo extendido


En el Capítulo 8, nos abrimos paso a travé s de una jerarquía de hipotecas como
forma de ilustrar el uso de subclases. Concluimos ese capítulo observando que
“nuestro programa debería producir parcelas diseñ adas para mostrar có mo se
comporta la hipoteca a lo largo del tiempo”. La figura 11.1 mejora la clase
Hipoteca al agregar mé todos que facilitan la producció n de dichos grá ficos. (La
funció n findPayment, que se utiliza en Mortgage, se define en la figura 8.8.)

Los mé todos plotPayments y plotBalance son sencillos, pero usan una forma de pylab.plot
que aú n no hemos visto. Cuando una figura contiene varias parcelas, es ú til generar una
clave que identifique lo que se pretende que represente cada parcela. En la Figura 11.1,
cada invocació n de pylab.plot usa el argumento de la palabra clave label para asociar una
cadena con la trama producida por esa invocació n. (Este y otros argumentos de palabra
clave deben seguir cualquier cadena de formato). Luego se puede agregar una clave a la
figura llamando a la funció n pylab.legend, como se muestra en la Figura 11.3.

Los mé todos no triviales en la clase Mortgage son plotTotPd y plotNet. El método


plotTotPd simplemente traza el total acumulado de los pagos realizados. El mé todo
plotNet traza una aproximació n al costo total de la hipoteca a lo largo del tiempo trazando
el efectivo gastado menos el capital adquirido al pagar parte del pré stamo.62

62Esuna aproximació n porque no realiza un cá lculo del valor presente neto para tener en
cuenta el valor del efectivo en el tiempo.
Capítulo 11. Trazado y más sobre las 1

clase Hipoteca(objeto):
"""Clase abstracta para construir diferentes tipos de hipotecas"""

def __init__(self, préstamo, annRate, meses):


"""Crear una nueva hipoteca"""
self.prestamo = prestamo
self.rate = annRate/12.0
self.months = meses
self.paid = [0.0]
self.owned = [prestamo]
self.pago = findPayment(préstamo, self.rate, meses)
self.legend = Ninguno #descripción de la hipoteca

def realizarPago(auto):
"""Hacer un pago"""
auto.pago.append(auto.pago)
reducción = auto.pago - auto.deuda[-1]*auto.tarifa
auto.deuda.append(auto.deuda[-1] - reducción)

def getTotalPaid(self):
"""Devolver el monto total pagado hasta
el momento""" devolver la suma (pago
propio)

definitivamente __str__(self):
return
self.leyenda

def plotPayments(self, estilo):


pylab.plot(auto.pagado[1:], estilo, etiqueta = auto.leyenda)

def plotBalance(self, estilo):


pylab.plot(auto.deuda, estilo, etiqueta = auto.leyenda)

def plotTotPd(self, estilo):


"""Represente el total acumulado de los pagos
realizados""" totPd = [self.paid[0]]
for i in range(1, len(self.paid)):
totPd.append(totPd[-1] + self.paid[i])
pylab.plot(totPd, estilo, etiqueta = self.leyenda)

def plotNet(self, estilo):


"""Trace una aproximación al costo total de la hipoteca a lo
largo del tiempo trazando el efectivo gastado menos el
capital adquirido al pagar parte del préstamo"""
totPd = [self.paid[0]]
for i in range(1, len(self.paid)):
totPd.append(totPd[-1] + self.paid[i])
#El capital adquirido a través de los pagos es el monto del préstamo
original
# pagado hasta la fecha, que es el monto del préstamo menos lo
que aún se debe capital adquirido =
pylab.array([auto.préstamo]*len(auto.deuda)) capital adquirido =
capital adquirido - pylab.array(auto.deuda)
net = pylab.array(totPd) - equityAcquired
pylab.plot(net, style, label = self.legend)

Figura 11.1 Class Mortgage con métodos de trazado

La expresió n pylab.array(self.owed) en plotNet realiza una conversió n de tipos. Hasta


ahora, hemos llamado a las funciones de trazado de PyLab con argumentos de tipo lista.
Bajo las sá banas, PyLab ha estado convirtiendo estas listas a una diferente
1 Capítulo 11. Trazado y más sobre las

tipo, matriz, que PyLab hereda de NumPy.63 La invocació n pylab.array hace esto
explícito. Hay varias formas convenientes de manipular arreglos que no está n
disponibles para las listas. En particular, las expresiones se pueden formar utilizando
matrices y operadores aritméticos. Consideremos, por ejemplo, el có digo
a1 = pylab.array([1, 2, 4])
imprime 'a1 =', a1
a2 = a1*2
imprimir 'a2 =', a2
imprime 'a1 + 3 =', a1 + 3
imprime '3 - a1 =', 3 - a1
imprime 'a1 - a2 =', a1 -
a2 imprime 'a1*a2 =',
a1*a2

La expresió n a1*2 multiplica cada elemento de a1 por la constante 2. La expresió n


a1+3 suma el entero 3 a cada elemento de a1. La expresió n a1-a2 resta cada
elemento de a2 del elemento correspondiente de a1 (si las matrices hubieran sido de
diferente longitud, habría ocurrido un error). La expresion
a1*a2 multiplica cada elemento de a1 por el correspondiente elemento de
a2. Cuando se ejecuta el código anterior, se imprime

a1 = [1 2 4]
a2 = [2 4 8]
a1 + 3 = [4 5 7]
3 - a1 = [ 2 1 -1]
a1 - a2 = [-1 -2 -4]
a1*a2 = [2 8 32]

Hay varias formas de crear matrices en PyLab, pero la forma má s comú n es crear
primero una lista y luego convertirla.

La figura 11.2 repite las tres subclases de Hipoteca deCapítulo 8. Cada uno tiene un
distintivoen esoque anula elen esoenHipoteca. la subclaseDosTasatambién anula
lahacer el pagométodo deHipoteca.

63NumPyes un módulo de Python que proporciona herramientas para la


computación científica. Además de proporcionar matrices
multidimensionales, proporciona una variedad de herramientas de
álgebra lineal.
Capítulo 11. Trazado y más sobre las 1

clase Fijo(Hipoteca):
def __init__(self, préstamo, r, meses):
Hipoteca.__init__(self, préstamo, r, meses)
self.legend = 'Fixed, ' + str(r*100) + '%'

clase FixedWithPts(hipoteca):
def __init__(self, préstamo, r, meses, pts):
Hipoteca.__init__(self, préstamo, r, meses)
self.pts = pts
self.paid = [prestamo*(pts/100.0)]
self.leyenda = 'Fijo, ' + str(r*100) + '%, '\
+ str(pts) + 'puntos'

clase TwoRate(Hipoteca):
def __init__(self, préstamo, r, meses, teaserRate, teaserMonths):
Hipoteca.__init__(self, préstamo, teaserRate, meses)
self.teaserMonths = teaserMonths
self.teaserRate = teaserRate
self.nextRate = r/12.0
self.legend =
str(teaserRate*100)\
+ '% para ' + str(self.teaserMonths)\
+ ' meses, luego ' + str(r*100) + '%'

def hacerPago(auto):
if len(self.paid) == self.teaserMonths + 1:
self.rate = self.nextRate
auto.pago = findPayment(auto.deuda[-1], auto.tasa,
self.months - self.teaserMonths)

Figura 11.2 Subclases deHipoteca

La figura 11.3 contiene funciones que se pueden usar para generar grá ficos
destinados a brindar informació n sobre los diferentes tipos de hipotecas.

La funció n plotMortgages genera títulos apropiados y etiquetas de eje para cada


parcela y luego usa los mé todos en MortgagePlots para producir las parcelas reales.
Utiliza llamadas a pylab.figure para garantizar que las grá ficas apropiadas aparezcan
en una figura determinada. Utiliza el índice i para seleccionar elementos de las listas
morts y estilos de una manera que garantiza que los diferentes tipos de hipotecas se
representen de manera consistente en todas las cifras. Por ejemplo, dado que el tercer
elemento en morts es una hipoteca de tasa variable y el tercer elemento en estilos es
'b:', la hipoteca de tasa variable siempre se traza utilizando una línea de puntos azul.

La funció n compareMortgages genera una lista de diferentes hipotecas y simula


hacer una serie de pagos en cada una, como lo hizo en el Capítulo 8. Luego llama a
plotMortgages para producir los grá ficos.
1 Capítulo 11. Trazado y más sobre las

def plotMortgages(morts, amt):


estilos = ['b-', 'b-.', 'b:']
#Dar nombres a números de
cifras pagos = 0
costo = 1
saldo = 2
netCost = 3
pylab.figure(pagos)
pylab.title('Pagos mensuales de diferentes $' + str(amt)
+ ' Hipotecas')
pylab.xlabel('Meses')
pylab.ylabel('Pagos Mensuales')
pylab.figure(coste)
pylab.title('Desembolso de efectivo de diferentes $' + str(importe) +
'Hipotecas') pylab.xlabel('Meses')
pylab.ylabel('Total de pagos')
pylab.figure(saldo)
pylab.title('Saldo restante de $' + str(importe) + 'Hipotecas')
pylab.xlabel('Meses')
pylab.ylabel('Saldo restante del préstamo de $')
pylab.figure(netCost)
pylab.title('Coste neto de $' + str(importe) + 'Hipotecas')
pylab.xlabel('Meses')
pylab.ylabel('Pagos - Equidad $')
for i in range(len(morts)):
pylab.figure(pagos)
morts[i].plotPayments(estilos[i])
pylab.figure(costo)
morts[i].plotTotPd(estilos[i])
pylab.figure(saldo)
morts[i].plotBalance(estilos [i])
pylab.figure(netCost)
morts[i].plotNet(estilos[i])
pylab.figure(pagos)
pylab.legend(loc = 'centro
superior') pylab.figure(costo)
pylab.legend(loc = 'mejor')
pylab.figure(saldo)
pylab.legend(loc = 'mejor')

def compareMortgages(amt, years, fixedRate, pts, ptsRate,


varRate1, varRate2, varMeses):
totMeses = años*12
fijo1 = Fijo(cantidad, tasafija, totMeses)
fijo2 = FijoConPtos(amt, ptsRate, totMonths, pts)
dosRate = TwoRate(importe, varRate2, totMonths, varRate1, varMonths)
morts = [fixed1, fixed2, twoRate]
for m in range(totMonths):
for mort in morts:
mort.makePayment()
plotMortgages(morts, amt)

Figura 11.3 Generar gráficos de hipotecas

La llamada
compareMortgages(amt=200000, years=30, fixedRate=0.07,
pts = 3,25, ptsRate=0,05,
tasavar1=0,045, tasavar2=0,095, mesesvar=48)
Capítulo 11. Trazado y más sobre las 1

produce parcelas que arrojan algo de luz sobre las hipotecas discutidas en el Capítulo 8.

El primer diagrama, que fue


producido por invocaciones de
plotPayments, simplemente
representa cada pago de cada
hipoteca contra el tiempo. El cuadro
que contiene la clave aparece donde
lo hace debido al valor
proporcionado al argumento de
palabra clave loc utilizado en la
llamada a pylab.legend. Cuando loc
está vinculado a 'mejor', la ubicació n
se elige automá ticamente. Esta
grá fica deja en claro có mo los pagos
mensuales varían (o no) con el
tiempo,
pero no arroja mucha luz sobre los costos relativos de cada tipo de hipoteca.

La trama siguiente fue producida por invocaciones de plotTotPd. Arroja algo de luz
sobre el costo de cada tipo de hipoteca al trazar los costos acumulados en los que se
ha incurrido al comienzo de cada mes. Toda la trama está a la izquierda, y una
ampliació n de la parte izquierda de la trama está a la derecha.

Los siguientes dos grá ficos muestran la deuda restante (a la izquierda) y el costo
neto total de tener la hipoteca (a la derecha).
12 PROGRAMAS ESTOCÁSTICOS, PROBABILIDAD Y
ESTADÍSTICAS
Hay algo muy reconfortante en la mecá nica newtoniana. Empujas hacia abajo un
extremo de una palanca y el otro extremo sube. Lanzas una pelota al aire; recorre
una trayectoria parabó lica y desciende. 𝐹 = 𝑚𝑎. En resumen, todo sucede por una
razó n. El mundo físico es completamente predecible.
lugar: todos los estados futuros de un sistema físico pueden derivarse del
conocimiento sobre su estado actual.
Durante siglos, esta fue la sabiduría científica predominante; luego llegaron la
mecá nica cuá ntica y la Doctrina de Copenhague. Los defensores de la doctrina,
encabezados por Bohr y Heisenberg, argumentaron que, en su nivel má s
fundamental, no se puede predecir el comportamiento del mundo físico. Uno
puede hacer enunciados probabilísticos de la forma "es muy probable que
ocurra x", pero no enunciados de la forma "es seguro que ocurra x". Otros físicos
distinguidos, sobre todo Einstein y Schrö dinger, discreparon vehementemente.

Este debate sacudió los mundos de la física, la filosofía e incluso la religió n. El


corazó n del debate fue la validez del no determinismo causal, es decir, la creencia
de que no todos los eventos son causados por eventos anteriores. Einstein y
Schrö dinger encontraron esta visió n filosó ficamente inaceptable, como lo
ejemplifica el comentario repetido de Einstein: "Dios no juega a los dados". Lo
que podían aceptar era el no determinismo predictivo, es decir, el concepto de
que nuestra incapacidad para hacer mediciones precisas sobre el mundo físico
hace imposible hacer predicciones precisas sobre estados futuros. Esta
distinció n fue muy bien resumida por Einstein, quien dijo: "El cará cter
esencialmente estadístico de la teoría contemporá nea debe atribuirse
ú nicamente al hecho de que esta teoría opera con una descripció n incompleta de
los sistemas físicos".

La cuestió n del no determinismo causal sigue sin resolverse. Sin embargo, si la


razó n por la que no podemos predecir eventos es porque son realmente
impredecibles o porque no tenemos suficiente informació n para predecirlos, no
tiene importancia prá ctica. Si bien el debate Bohr/Einstein fue sobre có mo
comprender los niveles má s bajos del mundo físico, surgen los mismos problemas a
nivel macroscó pico. Tal vez los resultados de las carreras de caballos, los giros de
las ruedas de la ruleta y las inversiones en el mercado de valores sean causalmente
deterministas. Sin embargo, existe amplia evidencia de que es peligroso tratarlos
como predeciblemente deterministas.64

Este libro trata sobre el uso de la computació n para resolver problemas. Hasta
ahora, hemos centrado nuestra atenció n en problemas que pueden resolverse
mediante un cá lculo determinista predecible. Dichos cá lculos son muy ú tiles,
pero claramente no son suficientes para abordar algunos tipos de problemas.
Muchos aspectos del mundo en

64Por supuesto, esto no impide que las personas crean que lo son y pierdan mucho dinero en
base a esa creencia.
Capítulo 12. Programas estocásticos, probabilidad y 1

que vivimos se puede modelar con precisió n só lo como procesos estocá sticos65.
Un proceso es estocá stico si su pró ximo estado depende tanto de los estados
previos como de algú n elemento aleatorio.

12.1 Programas estocásticos


Un programa es determinista si cada vez que se ejecuta en la misma entrada,
produce la misma salida. Observe que esto no es lo mismo que decir que la
salida está completamente definida por la especificació n del problema.
Considere, por ejemplo, la especificació n de raíz cuadrada:
def raíz cuadrada(x, épsilon):
"""Asume que x y epsilon son de tipo float; x >= 0 y epsilon > 0 Devuelve
float y tal que x-epsilon <= y*y <= x+epsilon"""

Esta especificació n admite muchos valores de retorno posibles para la funció n


llamada raízcuadrada(2, 0.001). Sin embargo, el algoritmo de aproximació n
sucesiva que vimos en el Capítulo 3 siempre devolverá el mismo valor. La
especificació n no requiere que la implementació n sea determinista, pero permite
implementaciones deterministas.

No todas las especificaciones interesantes pueden cumplirse mediante


implementaciones deterministas. Considere, por ejemplo, implementar un
programa para jugar un juego de dados, digamos backgammon o dados. En
alguna parte del programa puede haber una funció n que simule una tirada
justa66 de un solo dado de seis caras. Supongamos que tuviera una
especificació n algo así como
def rodarMorir():
"""Devuelve un int entre 1 y 6"""

Esto sería problemá tico, ya que permite que la implementació n devuelva el mismo
nú mero cada vez que se llama, lo que haría que el juego fuera bastante aburrido.
Sería mejor especificar querodarDie“devuelve un int elegido aleatoriamente
entre 1 y 6.”

La mayoría de los lenguajes de programació n, incluido Python, incluyen formas


sencillas de escribir programas que utilizan la aleatoriedad. El có digo de la figura
12.1 utiliza una de varias funciones ú tiles que se encuentran en el mó dulo aleatorio
de la biblioteca está ndar de Python importado. La funció n random.choice toma una
secuencia no vacía como argumento y devuelve un miembro elegido al azar de esa
secuencia. Casi todas las funciones en random se construyen usando la funció n
random.random, que genera un nú mero de punto flotante aleatorio entre 0.0 y
1.0.67

sesenta y cincoLa
palabra proviene de la palabra griega stokhastikos, que significa algo así
como "capaz de adivinar". Un programa estocá stico, como veremos, está orientado a
obtener un buen resultado, pero no se garantizan los resultados exactos.
66Una tirada es justa si cada uno de los seis resultados posibles es igualmente probable.
67De hecho, la funció n no es realmente aleatoria. Es lo que los matemá ticos llaman
pseudoaleatorio. Para casi todos los propó sitos prá cticos fuera de la criptografía, esta
distinció n no es relevante y la ignoraremos.
1 Capítulo 12. Programas estocásticos, probabilidad y

importar al azar

def rodarMorir():
"""Devuelve un int aleatorio entre 1 y 6"""
return random.choice([1,2,3,4,5,6])

def rollN(n):
resultado = ''
para i en el rango (n):
resultado = resultado +
str(rollDie()) imprimir resultado

Figura 12.1 Tirar dado

Ahora, imagina ejecutar rollN(10). ¿Te sorprendería má s verlo impreso


1111111111 o 5442462412? O, dicho de otro modo, ¿cuá l de estas dos secuencias
es má s aleatoria? Es una pregunta trampa. Cada una de estas secuencias es
igualmente probable, porque el valor de cada lanzamiento es independiente de los
valores de los lanzamientos anteriores. En un proceso estocá stico, dos eventos son
independientes si el resultado de uno no influye en el resultado del otro.

Esto es un poco má s fá cil de ver si simplificamos la situació n pensando en un dado


de dos caras (tambié n conocido como moneda) con los valores 0 y 1. Esto nos
permite pensar en la salida de una llamada de rollN como un binario (ver Capítulo
3). Cuando usamos un dado binario, hay 2n secuencias posibles que testN podría
devolver. Cada uno deestos son igualmente probables; por lo tanto cada uno tiene una
probabilidad de ocurrir de (1/2)n.

Volvamos a nuestro dado de seis caras. ¿Cuá ntas secuencias diferentes hay de
longitud 10? 610. Entonces, la probabilidad de sacar diez unos consecutivos es
1/610. Menos de uno entre sesenta millones. Bastante baja, pero no má s baja que
la probabilidad de cualquier otra secuencia en particular, por ejemplo,
5442462412, de diez rollos.

En general, cuando hablamos de la probabilidad de que un resultado tenga alguna


propiedad (p. ej., todos los 1) estamos preguntando qué fracció n de todos los
resultados posibles tiene esa propiedad. Esta es la razó n por la que las
probabilidades varían de 0 a 1. Supongamos que queremos saber la probabilidad de
obtener cualquier secuencia que no sea solo 1 al lanzar el dado. Es sencillo1 –
(1/610), porque la probabilidad de que suceda algo y la probabilidad de que no suceda lo mismo
debe sumar 1.

Supongamos que queremos saber la probabilidad de tirar el dado diez veces sin
obtener un solo 1. Una forma de responder a esta pregunta es transformarla en la
pregunta de cuá ntas de las 610 secuencias posibles no contienen un 1.
Capítulo 12. Programas estocásticos, probabilidad y 1

Esto se puede calcular de la siguiente manera:

 La probabilidad de no tirar un1en cualquier rollo individual es5/6.


 La probabilidad de no sacar un 1 ni en la primera ni en la segunda tirada es
(5/6)*(5/6),o(5/6)2.

 Entonces,la probabilidad de no sacar 1 diez veces seguidas es(5/6)10,un poco má s


que0.16.
Volveremos al tema de la probabilidad con un poco má s de detalle má s adelante.

12.2 Estadística Inferencial y Simulación


El diminuto programa de la figura 12.1 es un modelo de simulació n. En lugar de
pedirle a una persona que tire un dado varias veces, escribimos un programa
para simular esa actividad.

A menudo usamos simulaciones para estimar el valor de una cantidad desconocida


haciendo uso de los principios de la estadística inferencial. En resumen (dado que
este no es un libro sobre estadística), el principio rector de la estadística inferencial
es que una muestra aleatoria tiende a exhibir las mismas propiedades que la
població n de la que se extrae.

Supongamos que Harvey Dent (también conocido como Two-Face) lanza una
moneda y sale cara. No se inferiría de esto que el pró ximo lanzamiento también
saldría cara. Supongamos que lo volteó dos veces y salió cara en ambas ocasiones.
Podría razonar que la probabilidad de que esto sucediera para una moneda justa (es
decir, una moneda en la que cara y cruz son igualmente probables) era de 0,25, por
lo que aú n no había razó n para suponer que el pró ximo lanzamiento sería cara.
Supongamos, sin embargo, que 100 de cada 100 lanzamientos salieron cara. 1/2100
es un nú mero bastante pequeñ o, por lo que puede sentirse seguro al inferir que la
moneda tiene cara en ambos lados.

Su creencia de que la moneda es justa se basa en la intuició n de que el


comportamiento de una muestra de 100 lanzamientos es similar al comportamiento
de la població n de todos los lanzamientos de su moneda. Esta creencia parece
bastante só lida cuando los 100 lanzamientos son cara.
Supongamos que 55 lanzamientos arrojaron cara y 45 cruces. ¿Te sentirías có modo
al predecir que los pró ximos 100 lanzamientos tendrían la misma proporció n de
caras y cruces? De hecho, ¿qué tan có modo se sentiría al predecir que habrá má s
caras que cruces en los pró ximos 100 lanzamientos? Tó mese unos minutos para
pensar en esto y luego pruebe el experimento utilizando el có digo de la figura 12.2.

La funció n flip de la figura 12.2 simula lanzar una moneda justa numFlips veces y
devuelve la fracció n de lanzamientos que dieron cara. Para cada lanzamiento,
random.random() devuelve un nú mero de punto flotante aleatorio entre 0,0 y 1,0.
Los nú meros menores o mayores que 0,5 se tratan como cara o cruz,
respectivamente. Al valor 0,5, se le asigna arbitrariamente el valor colas. Dada la
gran cantidad de valores de punto flotante entre 0,0 y 1,0, es muy poco probable
que esto afecte el resultado.
1 Capítulo 12. Programas estocásticos, probabilidad y

def
flip(numFlips
): cabezas =
0.0
para i en el rango (numFlips):
si random.random() <
0.5: cabezas += 1
devolver cabezas/numFlips

def flipSim(númFlipsPerTrial, numTrials):


fracHeads = []
for i in range(numTrials):
fracHeads.append(flip(numFlipsPerTrial))

Figura 12.2 Lanzar una moneda

Intente ejecutar la funció n flipSim(100, 1) un par de veces. Esto es lo que vimos las dos
primeras veces que lo probamos:
>>> flipSim(100, 1)
0.44
>>> flipSim(100, 1)
0.57999999999999996

Parece que sería inapropiado suponer mucho (aparte de que la moneda tiene
cara y cruz) de cualquier prueba de 100 lanzamientos. Es por eso que
normalmente estructuramos nuestras simulaciones para incluir mú ltiples
ensayos y comparar los resultados. Intentemos flipSim(100, 100):
>>> flipSim(100, 100)
0.4993
>>> flipSim(100, 100)
0.4953

Intuitivamente, podemos sentirnos mejor con estos resultados. Qué tal si


FlipSim(100, 100000):

>>> FlipSim(100, 1000000)


0.49999221
>>> FlipSim(100, 100000)
0.50003922

Esto se ve muy bien (especialmente porque sabemos que la respuesta debería


ser 0.5, pero eso es hacer trampa). Ahora parece que podemos concluir con
seguridad algo sobre el siguiente lanzamiento, es decir, que cara y cruz son
igualmente probables. Pero, ¿por qué pensamos que podemos concluir eso?

De lo que dependemos es de la ley de los grandes nú meros (también conocida


como teorema de Bernoulli68). Esta ley establece que en experimentos
independientes repetidos (p. ej., lanzar una moneda al aire 100 veces y contar la
fracció n de caras) con el mismo valor esperado (0.5 en este caso), el valor
promedio de la

68Aunque la ley de los grandes nú meros había sido discutida en el 16elsiglo por Cardano, la
primera prueba fue publicada por Jacob Bernoulli a principios del 18 elsiglo. No tiene relació n
con el teorema sobre diná mica de fluidos llamado teorema de Bernoulli, que fue probado por
el sobrino de Jacob, Daniel.
Capítulo 12. Programas estocásticos, probabilidad y 1

experimentos se aproxima al valor esperado a medida que el nú mero de


experimentos tiende a infinito.

Vale la pena señ alar que la ley de los grandes nú meros no implica, como muchos
parecen pensar, que si ocurren desviaciones del comportamiento esperado, es
probable que estas desviaciones sean compensadas por desviaciones opuestas en el
futuro. Esta mala aplicació n de la ley de los grandes nú meros se conoce como la
falacia del jugador. 69

Tenga en cuenta que "grande" es un concepto relativo. Por ejemplo, si


lanzá ramos una moneda justa del orden de 101.000.000 de veces, esperaríamos
encontrar varias secuencias de al menos un milló n de caras consecutivas. Si
observá ramos solo el subconjunto de lanzamientos que contienen estas caras,
inevitablemente llegaríamos a una conclusió n erró nea sobre la equidad de la
moneda. De hecho, si cada subsecuencia de una gran secuencia de eventos parece
ser aleatoria, es muy probable que la secuencia en sí misma no sea
verdaderamente aleatoria. Si su modo aleatorio de iTunes no reproduce la misma
canció n primero de vez en cuando, puede asumir que la reproducció n aleatoria
no es realmente aleatoria.

Finalmente, observe que en el caso de lanzamientos de monedas, la ley de los


grandes nú meros no implica que la diferencia absoluta entre el nú mero de caras y
cruces disminuya a medida que aumenta el nú mero de lanzamientos. De hecho,
podemos esperar que ese nú mero aumente. Lo que disminuye es la relació n entre la
diferencia absoluta y el nú mero de lanzamientos.

La figura 12.3 contiene una funció n, flipPlot, que produce algunas grá ficas
destinadas a mostrar la ley de los grandes nú meros en acció n. La línea
random.seed(0) cerca de la parte inferior asegura que el generador de nú meros
pseudoaleatorios utilizado por random.random generará la misma secuencia de
nú meros pseudoaleatorios cada vez que se ejecute este có digo. Esto es
conveniente para la depuració n.

69“El 18 de agosto de 1913, en el casino de Montecarlo, las negras salieron un ré cord


veintisé is veces seguidas [en la ruleta]. … [Hubo] una carrera casi de pá nico para apostar
por el rojo, comenzando cuando el negro había salido unas quince veces fenomenales. En
aplicació n de la doctrina de la madurez [de las oportunidades], los jugadores duplicaron y
triplicaron sus apuestas, esta doctrina los llevó a creer despué s de que las negras salieron
por vigé sima vez que no había una posibilidad en un milló n de otra repetició n. Al final, la
carrera inusual enriqueció al Casino en algunos millones de francos”. Huff y Geis,Cómo
tomar una oportunidad, pá gs. 28-29.
1 Capítulo 12. Programas estocásticos, probabilidad y

def flipPlot(minExp, maxExp):


"""Asume números enteros positivos minExp y maxExp; minExp <
maxExp Traza resultados de 2**minExp a 2**maxExp lanzamientos
de moneda"""
proporcio
nes = []
diferenci
as = []
xAxis =
[]
para exp en rango (minExp, maxExp +
1): xAxis.append (2 ** exp)
para numFlips en
xAxis: numHeads
= 0
para n en el rango (numFlips):
si random.random() <
0.5: numHeads += 1
numTails = numFlips -
numHeadsratios.append(numHeads/float(numTails))
diffs.append(abs(numHeads - numTails))
pylab.title('Diferencia entre caras y cruces')
pylab.xlabel('Número de vueltas')
pylab.ylabel('Abs(#caras - #cruces)')
pylab.plot(xEje, diferencias)
pylab.figure() pylab.title('Razones
cara/cruz') pylab.xlabel('Número de
vueltas')
pylab.ylabel('#Cara/#Cora')
Figura 12.3 Representación gráfica de los resultados de
lanzamientos de monedas

La llamadaflipPlot(4, 20)produce las dos parcelas:

El grá fico de la izquierda parece sugerir que la diferencia absoluta entre el


nú mero de caras y el nú mero de cruces fluctú a al principio, se precipita hacia
abajo y luego se mueve rá pidamente hacia arriba. Sin embargo, debemos tener
en cuenta que solo tenemos dos puntos de datos a la derecha de x = 300,000. Que
pylab.plot conecte estos puntos con líneas puede engañ arnos para ver tendencias
cuando todo lo que tenemos son puntos aislados. Este no es un fenó meno poco
comú n, por lo que siempre debe preguntar cuá ntos puntos contiene realmente
una trama antes de llegar a una conclusió n sobre lo que significa.
Capítulo 12. Programas estocásticos, probabilidad y 1

Es difícil ver mucho de nada en la trama de la derecha, que es principalmente una


línea plana. Esto también es engañ oso. Aunque hay diecisé is puntos de datos, la
mayoría de ellos está n agrupados en una pequeñ a cantidad de bienes raíces en el
lado izquierdo de la grá fica, por lo que es imposible ver los detalles. Esto ocurre
porque los valores en el eje x van de 16 a 1,0485,76 y, a menos que se indique lo
contrario, PyLab espaciará estos puntos uniformemente a lo largo del eje. Esto se
llama escalado lineal.

Afortunadamente, estos problemas de visualizació n son fá ciles de abordar en


PyLab. Como vimos en el Capítulo 11, podemos indicar fá cilmente a nuestro
programa que trace puntos no conectados, por ejemplo, escribiendo
pylab.plot(xAxis, diffs, 'bo').

También podemos indicarle a PyLab que use una escala logarítmica en uno o ambos
de losXyyejes llamando a las funcionespylab.semilogxypylab.semilogía. Estas
funciones siempre se aplican a la figura actual.

Ambos grá ficos utilizan una escala logarítmica en el eje x. Dado que los valores
de x generados por flipPlot son 2minExp, 2minExp+1, .., 2maxExp, el uso de un
eje x logarítmico hace que los puntos esté n espaciados uniformemente a lo largo
del eje x, lo que proporciona la má xima separació n entre puntos. La siguiente
grá fica de la izquierda tambié n usa una escala logarítmica en el eje y. Los valores
de y en este grá fico oscilan entre casi 0 y casi 1000. Si el eje y tuviera una escala
lineal, sería difícil ver las diferencias relativamente pequeñ as en los valores de y
en el lado izquierdo del grá fico. Por otro lado, en el grá fico de la derecha, los

valores de y está n agrupados de manera bastante estrecha, por lo que usamos un


eje y lineal.

Ejercicio de dedos:Modifique el có digo de la figura 12.3 para que produzca


diagramas como los que se muestran arriba.

Estos grá ficos son má s fá ciles de interpretar que los grá ficos anteriores. El grá fico
de la derecha sugiere con bastante fuerza que la proporció n de caras y cruces
converge a 1,0 a medida que aumenta el nú mero de lanzamientos. El significado de
la trama de la izquierda es un poco menos claro. Parece que la diferencia absoluta
crece con el nú mero de lanzamientos, pero no es del todo convincente.

Nunca es posible lograr una precisió n perfecta a travé s del muestreo sin
muestrear a toda la població n. No importa cuá ntas muestras examinemos, nunca
podemos estar seguros de que el conjunto de muestras sea típico hasta que
1 Capítulo 12. Programas estocásticos, probabilidad y
examinemos cada elemento.
Capítulo 12. Programas estocásticos, probabilidad y 1

de la població n (y dado que normalmente tratamos con poblaciones infinitas, por


ejemplo, todas las secuencias posibles de lanzamientos de monedas, esto suele ser
imposible). Por supuesto, esto no quiere decir que una estimació n no pueda ser
exactamente correcta. Podríamos lanzar una moneda dos veces, obtener una cara y
una cruz y concluir que la verdadera probabilidad de cada una es 0.5. Habríamos
llegado a la conclusió n correcta, pero nuestro razonamiento habría sido erró neo.

¿Cuá ntas muestras necesitamos mirar antes de que podamos tener una confianza
justificada en nuestra respuesta? Esto depende de la varianza en la distribució n
subyacente.
En té rminos generales, la varianza es una medida de cuá nta dispersió n hay en los
diferentes resultados posibles.

Podemos formalizar esta noció n de manera relativamente simple utilizando el


concepto de desviació n está ndar. De manera informal, la desviació n está ndar
nos dice qué fracció n de los valores está n cerca de la media. Si muchos valores
está n relativamente cerca de la media, la desviació n está ndar es relativamente
pequeñ a. Si muchos valores está n relativamente lejos de la media, la desviació n
está ndar es relativamente grande. Si todos los valores son iguales, la desviació n
está ndar es cero.

Má s formalmente, la desviació n está ndar,  (sigma), de una colecció n de valores, X, es

definido como 𝜎 X 1
=2 |K|
𝑥𝗀K(𝑥 − 𝜇)2, donde |X| es el tamaño de la colección y 𝜇

(mu) su media. La figura 12.4 contiene una implementació n de Python de la


desviació n está ndar.70 Aplicamos la conversió n de tipo flotante, porque si cada uno
de los elementos de X es un int, el tipo de la suma será un int.

Figura 12.4 Desviación estándar


def desvstd(X):
Podemos """Asume que n
usar la noció X de
esdesviació
una lista de ndar
n está números.
para pensar en la relació n entre el
Devuelve la desviación estándar de X"""
nú mero de muestras que hemos observado y cuá nta confianza debemos tener en la
mean = float(sum(X))/len(X)
respuesta que
tot = hemos
0.0 calculado. La figura 12.5 contiene una versió n modificada de
flipPlot.para x en
Ejecuta varias pruebas de cada nú mero de lanzamientos de monedas y traza
X: de abs (cara - cruz) y la relació n cara/cruz. Tambié n traza la desviació n
las medias
total += (x - media)**2
está ndar de cada uno.

70Probablemente nunca necesite implementar esto usted mismo. Las bibliotecas


estadísticas implementan esta y muchas otras funciones estadísticas está ndar. Sin embargo,
presentamos el có digo aquí por si acaso algunos lectores prefieren mirar el có digo a mirar
las ecuaciones.
1 Capítulo 12. Programas estocásticos, probabilidad y

La implementació n de flipPlot1 usa dos funciones auxiliares. La funció n makePlot


contiene el có digo usado para producir los grá ficos. La funció n runTrial simula
una prueba de monedas numFlips.

def makePlot(xVals, yVals, title, xLabel, yLabel, style,


logX = False, logY = False):
"""Traza xVals frente a yVals con títulos y etiquetas
proporcionados.""" pylab.figure()
pylab.title(título)
pylab.xlabel(xLabel)
pylab.ylabel(yLabel)
pylab.plot(xVals, yVals,
estilo) si logX:
pylab.semilogx()
si logY:
pylab.semilogía()

def runTrial(numFlips):
numHeads = 0
para n en el rango (numFlips):
si random.random() < 0.5:
numHeads += 1
numTails = numFlips - numHeads
return (numHeads, numTails)

def flipPlot1(minExp, maxExp, numTrials):


"""Asume minExp y maxExp enteros positivos; minExp < maxExp
numPrueba un entero positivo
Traza resúmenes de los resultados de numTrials
ensayos de 2**minExp a 2**maxExp lanzamientos de
moneda"""
ratiosMeans, diffsMeans, ratiosSDs, diffsSDs = [], [], [], []
xAxis = []
para exp en rango (minExp, maxExp +
1): xAxis.append (2 ** exp)
para numFlips en
xAxis:
proporciones = []
diferencias = []
para t en el rango (número de ensayos):
numHeads, numTails =
runTrial(numFlips)ratios.append(numHeads
/float(numTails))
diffs.append(abs(numHeads - numTails))
ratiosMeans.append(sum(ratios)/float(numTrials))
diffsMeans.append(sum(diffs)/float(numTrials))
ratiosSDs.append(stdDev(ratios))
diffsSDs.append(stdDev(diffs))
numTrialsString = ' (' + str(numTrials) + ' Trials)'
title = 'Proporción media de caras/cruces' +
numTrialsString makePlot(xEje, ratiosMeans, title,
'Número de lanzamientos', 'Promedio de caras/cruces',
'bo', logX = True) title = 'SD caras/cruces ratios' +
numTrialsString
makePlot(xAxis, proporcionesSDs, título,
'Número de vueltas', 'Desviación estándar',
'bo', logX = Verdadero, logY = Verdadero)

Figura 12.5 Simulación de lanzamiento de monedas


Capítulo 12. Programas estocásticos, probabilidad y 1

IntentemosflipPlot1(4, 20, 20). Genera las parcelas

Esto es alentador. La relació n cara/cruz converge hacia 1 y el logaritmo de la


desviació n está ndar cae linealmente con el logaritmo del nú mero de lanzamientos
por prueba. Para cuando lleguemos a unos 106 lanzamientos de moneda por intento,
la desviació n está ndar (alrededor de10-3) es aproximadamente tres ó rdenes decimales de
magnitud menor que la media (alrededor de 1), lo que indica que la variació n entre los ensayos
fue pequeñ a. Por lo tanto, podemos tener una confianza considerable en que la relació n cara/cruz
esperada está bastante cerca de 1,0. A medida que lanzamos má s monedas, no solo tenemos una
respuesta má s precisa, sino que, lo que es má s importante, también tenemos motivos para estar
má s seguros de que se acerca a la respuesta correcta.

¿Qué pasa con la diferencia absoluta entre el nú mero de caras y el nú mero de


cruces? Podemos echarle un vistazo agregando al final de flipPlot1 el có digo de la
figura 12.6.

Figura 12.6
title = 'Mean abs(#Heads Diferencias
- #Tails)' absolutas
+ numTrialsString
makePlot(xEje, diffsMeans, title,
'Número de lanzamientos', 'Abs medias (#caras -
#cruces)', 'bo', logX = Verdadero, logY = Verdadero)
title = 'SD abs(#Heads - #Tails)' + numTrialsString
makePlot(xEje, diffsSDs, title,
'Número de vueltas', 'Desviación estándar',
'bo', logX = Verdadero, logY = Verdadero)
1 Capítulo 12. Programas estocásticos, probabilidad y

Esto produce las parcelas adicionales

Como era de esperar, la diferencia absoluta entre el nú mero de caras y cruces crece
con el nú mero de lanzamientos. Ademá s, dado que estamos promediando los
resultados de veinte ensayos, la grá fica es considerablemente má s fluida que
cuando trazamos los resultados de una sola prueba. Pero, ¿qué pasa con la ú ltima
trama? La desviació n está ndar crece con el nú mero de vueltas. ¿Significa esto que a
medida que aumenta el nú mero de lanzamientos deberíamos tener menos confianza
en la estimació n del valor esperado de la diferencia entre cara y cruz?

No, no lo hace. La desviació n está ndar siempre debe verse en el contexto de la


media. Si la media fuera mil millones y la desviació n está ndar 100, veríamos la
dispersió n de los datos como pequeñ a. Pero si la media fuera 100 y la desviació n
está ndar 100, veríamos la dispersió n bastante grande.

El coeficiente de variació n es la desviació n está ndar dividida por la media. Cuando


se comparan conjuntos de datos con medias muy variables (como aquí), el
coeficiente de variació n suele ser má s informativo que la desviació n está ndar.
Como puede ver en su implementació n en la Figura 12.7, el coeficiente de
variació n no está definido cuando la media es 0.

Figura 12.7 Coeficiente de variación


def CV(X):
media =
sum(X)/float(len(X))
prueba:
devuelve
stdDev(X)/mean excepto
Capítulo 12. Programas estocásticos, probabilidad y 1

La figura 12.8 contiene una versió n de flipPlot1 que traza los coeficientes de variació n.

def flipPlot1(minExp, maxExp, numTrials):


"""Asume minExp y maxExp enteros positivos; minExp < maxExp
numPrueba un entero positivo
Traza resúmenes de los resultados de numTrials
ensayos de 2**minExp a 2**maxExp lanzamientos de
moneda"""
ratiosMeans, diffsMeans, ratiosSDs, diffsSDs = [], [], [], []
ratiosCVs, diffsCVs = [], []
eje x = []
para exp en rango (minExp, maxExp +
1): xAxis.append (2 ** exp)
para numFlips en
xAxis:
proporciones = []
diferencias = []
para t en el rango (número de ensayos):
numHeads, numTails =
runTrial(numFlips)ratios.append(numHeads
/float(numTails))
diffs.append(abs(numHeads - numTails))
ratiosMeans.append(sum(ratios)/float(numTrials))
diffsMeans.append(sum(diffs)/float(numTrials))
ratiosSDs.append(stdDev(ratios))
diffsSDs.append(stdDev(diffs))
ratiosCVs.append( CV(ratios)) diffsCVs.append(CV(difs))
numTrialsString = ' (' + str(numTrials) + ' Trials)'
title = 'Proporción media de caras/cruces' +
numTrialsString makePlot(xEje, ratiosMeans, title,
'Número de lanzamientos', 'Promedio de caras/cruces',
'bo', logX = True) title = 'SD caras/cruces ratios' +
numTrialsString
makePlot(xAxis, proporcionesSDs, título,
'Número de vueltas', 'Desviación estándar',
'bo', logX = Verdadero, logY = Verdadero)
title = 'Mean abs(#Heads - #Tails)' + numTrialsString
makePlot(xEje, diffsMeans, title,
'Número de lanzamientos', 'Abs medias (#caras -
#cruces)', 'bo', logX = Verdadero, logY = Verdadero)
title = 'SD abs(#Heads - #Tails)' + numTrialsString
makePlot(xEje, diffsSDs, title,
'Número de vueltas', 'Desviación estándar',
'bo', logX = Verdadero, logY = Verdadero)
título = 'Coef. de Var. abs(#Heads - #Tails)' + numTrialsString
makePlot(xAxis, diffsCVs, title, 'Número de lanzamientos',
'Coef. de Var.', 'bo', logX = Verdadero)
título = 'Coef. de Var. Proporción caras/cruces' + numTrialsString
makePlot(xAxis, ratiosCVs, title, 'Number of Flips',
'Coef. de Var.', 'bo', logX = Verdadero, logY = Verdadero)

Figura 12.8 Versión final deflipPlot1


1 Capítulo 12. Programas estocásticos, probabilidad y

Produce las parcelas adicionales

En este caso, vemos que la grá fica del coeficiente de variació n de la relació n
cara/cruz no es muy diferente de la grá fica de la desviació n está ndar. Esto no es
sorprendente, ya que la ú nica diferencia entre los dos es la divisió n por la media, y
dado que la media está cerca de 1, la diferencia es pequeñ a.

Por otro lado, la grá fica del coeficiente de variació n de la diferencia absoluta entre
cara y cruz es una historia diferente. Se necesitaría una persona valiente para
argumentar que está tendiendo en cualquier direcció n. Parece estar fluctuando
ampliamente. Esto sugiere que la dispersió n en los valores de abs (cara – cruz) es
independiente del nú mero de lanzamientos. No está creciendo, como la
desviació n está ndar podría habernos hecho creer, pero tampoco está
disminuyendo. Tal vez aparecería una tendencia si intentá ramos 1000 intentos en
lugar de 20. Veamos.

Parece como si una vez que el nú mero


de lanzamientos alcanza alrededor de
1000, el coeficiente de variació n se
establece en algú n lugar en la vecindad
de
0,75. En general, las distribuciones con un
coeficiente de variación inferior a 1 se
consideran de baja varianza.

Tenga en cuenta que si la media es


cercana a cero, pequeñ os cambios en
la media conducen a cambios grandes
(pero no necesariamente
significativos) en el coeficiente de
variació n, y cuando la
media es cero, el coeficiente de variació n no está definido. Ademá s, como veremos
en breve, la desviació n está ndar se puede usar para construir un intervalo de
confianza, pero el coeficiente de variació n no.
Capítulo 12. Programas estocásticos, probabilidad y 1

12.3 Distribuciones
Un histograma es una grá fica diseñ ada para mostrar la distribució n de valores en un
conjunto de datos. Primero se ordenan los valores y luego se dividen en un nú mero
fijo de contenedores de igual ancho. Luego se dibuja una grá fica que muestra el
nú mero de elementos en cada contenedor. Consideremos, por ejemplo, el có digo
vals = [1, 200] #garantiza que los valores oscilarán entre 1 y
200 para i en el rango (1000):
num1 = random.choice(rango(1, 100))
num2 = random.choice(rango(1, 100))
vals.append(num1+num2)
pylab.hist(valores, contenedores = 10)

La llamada de funció npylab.hist(valores, contenedores = 10)produce el histograma,


con
diez contenedores, a la izquierda. PyLab
ha elegido automá ticamente el ancho de
cada contenedor. Mirando el có digo,
sabemos que el nú mero má s pequeñ o
en vals será 1 y el nú mero má s grande
200. Por lo tanto, los valores posibles en
el eje x van de 1 a 200. Cada contenedor
representa una fracció n igual de los
valores en el x - eje, por lo que el primer
contenedor contendrá los elementos 1-
20, el siguiente contenedor los
elementos 21-40, etc. Dado que los
valores medios elegidos para num1 y
num2 estará n en el
cerca de 50, no sorprende que haya má s elementos en los contenedores del
medio que en los contenedores cerca de los bordes.

A estas alturas debes estar terriblemente aburrido de lanzar monedas. Sin embargo,
vamos a pedirle que observe una simulació n má s de lanzamiento de monedas. La
simulació n en la Figura 12.9 ilustra má s de las capacidades de trazado de PyLab y
nos brinda la oportunidad de obtener una noció n visual de lo que significa la
desviació n está ndar.

La simulació n utiliza la funció n pylab.xlim para controlar la extensió n del eje x. La


llamada a la funció n pylab.xlim() devuelve una tupla compuesta por los valores
mínimo y má ximo del eje x de la figura actual. La llamada de funció n
pylab.xlim(xmin, xmax) establece los valores mínimo y má ximo del eje x de la figura
actual. La funció n pylab.ylim funciona de la misma manera.
1 Capítulo 12. Programas estocásticos, probabilidad y

def flip(numFlips):
cabezas = 0.0
para i en el rango (numFlips):
si random.random() < 0.5:
cabezas += 1
devolver cabezas/numFlips

def flipSim(númFlipsPerTrial, numTrials):


fracHeads = []
for i in range(numTrials):
fracHeads.append(flip(numFlipsPerTrial))
media =
sum(fracHeads)/len(fracHeads) sd =
stdDev(fracHeads)
retorno (fracHeads, media, sd)

def labelPlot(numFlips, numTrials, mean, sd):


pylab.title(str(numTrials) + ' ensayos de '
+ str(numFlips) + ' voltea cada
uno') pylab.xlabel('Fracción de cabezas')
pylab.ylabel('Número de pruebas')
xmin, xmax = pylab.xlim()
ymin, ymax = pylab.ylim()
pylab.text(xmin + (xmax-xmin)*0.02, (ymax-ymin)/2,
'Mean = ' + str(round(mean, 4))
+ '\nSD = ' + str(redonda(sd, 4)), tamaño='x-grande')

def makePlots(numFlips1, numFlips2, numTrials):


val1, mean1, sd1 = flipSim(numFlips1, numTrials)
pylab.hist(val1, bins = 20)
xmin,xmax = pylab.xlim()
ymin,ymax = pylab.ylim()
labelPlot(numFlips1, numTrials, mean1, sd1)
pylab.figure()
val2, media2, sd2 = flipSim(numFlips2, numTrials)
pylab.hist(val2, bins = 20)
pylab.xlim(xmín, xmáx)
labelPlot(numFlips2, numTrials, mean2, sd2)

aleatorio.seed(0)hacerPlots(100,100
0,100000)

Figura 12.9 Trazar histogramas que demuestran distribuciones normales

Cuando se ejecuta el có digo de la figura 12.9, produce las grá ficas


Capítulo 12. Programas estocásticos, probabilidad y 1

Observe que, si bien las medias en ambas grá ficas son casi iguales, las desviaciones
está ndar son bastante diferentes. La distribució n de los resultados es mucho má s
estrecha cuando lanzamos la moneda 1000 veces por intento que cuando lanzamos
la moneda 100 veces por intento.
Para aclarar esto, hemos usado pylab.xlim para forzar los límites del eje x en la
segunda grá fica para que coincidan con los de la primera grá fica, en lugar de dejar
que PyLab elija los límites. También hemos usado pylab.xlim y pylab.ylim para elegir
un conjunto de coordenadas para mostrar un cuadro de texto con la media y la
desviació n está ndar.

12.3.1 Distribuciones normales y niveles de confianza


La distribució n de los resultados en cada uno de estos grá ficos se acerca a lo que se
denomina una distribució n normal. Técnicamente hablando, una distribució n
normal se define mediante la fó rmula

1 (𝑥!𝜇)2
ƒ𝑥= ∗𝑒 ! 2𝜎2
𝜎 2𝜋

donde μ es la media, σ la desviació n está ndar y e el nú mero de Euler


(aproximadamente 2,718). Si no tienes ganas de estudiar esta ecuació n, está bien.
Solo recuerde que las distribuciones normales alcanzan su punto má ximo en la
media, caen simétricamente por encima y por debajo de la media y se acercan
asintó ticamente a 0. Tienen la agradable propiedad matemá tica de estar
completamente especificadas por dos pará metros: la media y la desviació n está ndar
(los ú nicos dos pará metros en la ecuacion).
Conocerlos es equivalente a conocer la distribució n completa. La forma de la
distribució n normal se parece (a los ojos de algunos) a la de una campana, por lo
que a veces se la denomina curva de campana.

Como podemos ver al hacer zoom en


el centro de la grá fica para 1000
lanzamientos/prueba, la distribució n
no es perfectamente simétrica y, por
lo tanto, no es del todo normal. Sin
embargo, a medida que aumentamos
el nú mero de ensayos, la distribució n
convergerá hacia la normalidad.

Las distribuciones normales se


utilizan con frecuencia en la
construcció n de modelos
probabilísticos por tres razones: 1)
tienen buenas propiedades
matemá ticas,
2) muchos de origen natural
las distribuciones son de hecho cercanas a la normal, y 3) pueden usarse para producir
intervalos de confianza.

En lugar de estimar un pará metro desconocido por un solo valor (p. ej., la media de
un conjunto de ensayos), un intervalo de confianza proporciona un rango que
probablemente contenga el valor desconocido y un grado de confianza de que el
valor desconocido se encuentra dentro de ese rango. Por ejemplo, una encuesta
política podría indicar que es probable que un candidato obtenga el 52 % de los
votos ±4 % (es decir, el intervalo de confianza es de tamañ o 8) con un nivel de
1 Capítulo 12. Programas estocásticos, probabilidad y
confianza del 95 %. Lo que esto significa es que la encuestadora cree que el 95% de
las veces el candidato recibirá entre el 48% y el 56% de los votos. Juntos, el intervalo
de confianza y el nivel de confianza indican la confiabilidad del
Capítulo 12. Programas estocásticos, probabilidad y 1

estimar. Casi siempre, aumentar el nivel de confianza ampliará el intervalo de


confianza.

El cá lculo de un intervalo de confianza generalmente requiere suposiciones sobre


la naturaleza del espacio que se está muestreando. Asume que la distribució n de
errores de estimació n es normal y tiene una media de cero. La regla empírica para
las distribuciones normales proporciona una forma prá ctica de estimar los niveles
e intervalos de confianza dada la media y la desviació n está ndar:

 68%de los datos caerá dentro1desviació n está ndar de la media,

 95%de los datos caerá dentro2desviaciones está ndar de la media, y

 casi todos (99,7%) de los datos caerá dentro3desviaciones está ndar de la


media.71
Supongamos que realizamos 100 intentos de 100 lanzamientos de monedas cada
uno. Suponga ademá s que la fracció n media de caras es 0,4999 y la desviació n
está ndar 0,0497. Si asumimos que la distribució n de las medias de las pruebas fue
normal, podemos concluir que si realizamos má s pruebas de 100 lanzamientos cada
una,

 95%del tiempo la fracció n de caras será 0,4999 ±0,0994y

 >99%del tiempo la fracció n de caras será 0,4999 ±0,1491.

Suele ser ú til visualizar los intervalos de confianza usando barras de error. El có digo
de la figura 12.10 llama a la versió n de flipSim de la figura 12.9 y luego usa
pylab.barra de error(xVals, significa, yerr = 2*pylab.array(sds))

para producir la trama de la derecha.


Los dos primeros argumentos dan los
valores de x e y que se van a graficar.
El tercer argumento dice que los
valores en sds deben usarse para
crear barras de error verticales. La
llamada

Mostrar Barras de Error (3, 10, 100)

produce la trama de la derecha. Como era


de esperar, las barras de error se reducen
a medida que aumenta el nú mero de
lanzamientos por prueba.

71Estosvalores son aproximaciones. Por ejemplo, el 95% de los datos caerá n dentro de 1.96
desviaciones está ndar de la media; 2 desviaciones está ndar es una aproximació n conveniente.
1 Capítulo 12. Programas estocásticos, probabilidad y

def mostrarBarrasError(minExp, maxExp, numTrials):


"""Asume minExp y maxExp enteros positivos; minExp < maxExp
numPrueba un entero positivo
Traza la fracción media de cabezas con barras de
error""" significa, sds = [], []
xValores = []
para exp en rango (minExp, maxExp +
1): xVals.append (2 ** exp)
fracHeads, media, sd = flipSim(2**exp, numTrials)
mean.append(media)
sds.append(sd)
pylab.errorbar(xVals,
significa,
yerr=2*pylab.array(sds))
pylab.semilogx()
pylab.title('Fracción media de caras (' + str(numTrials) + 'trials)')
pylab.xlabel('Número de lanzamientos por prueba')

Figura 12.10 Gráfico de producción con barras de error

Por supuesto, encontrar un buen modelo matemá ticamente no sirve de nada si


proporciona un mal modelo de los datos reales. Afortunadamente, muchas variables
aleatorias tienen una distribució n aproximadamente normal. Por ejemplo, las
propiedades físicas de plantas y animales (p. ej., altura, peso, temperatura corporal)
suelen tener distribuciones aproximadamente normales. Es importante destacar
que muchas configuraciones experimentales tienen errores de medició n
normalmente distribuidos. Esta suposició n fue utilizada a principios del siglo XIX
por el matemá tico y físico alemá n Karl Gauss, quien asumió una distribució n normal
de errores de medició n en su aná lisis de datos astronó micos (lo que llevó a que la
distribució n normal se conociera como distribució n gaussiana en gran parte de la
comunidad científica). ).

Las distribuciones normales se pueden generar fá cilmente llamando


aleatorio.gauss(mu, sigma) , que devuelve un nú mero de punto flotante elegido
al azar de una distribució n normal con mediamuy desviació n está ndarsigma.

Sin embargo, es importante recordar que no todas las distribuciones son normales.

12.3.2 Distribuciones Uniformes


Considere lanzar un solo dado. Cada uno de los seis resultados es igualmente
probable. Si uno lanzara un solo dado un milló n de veces y creara un histograma
que mostrara con qué frecuencia sale cada nú mero, cada columna tendría casi la
misma altura. Si uno tuviera que trazar la probabilidad de que se elija cada nú mero
de lotería posible, sería una línea plana (en 1 dividido por el rango de los nú meros
de lotería). Tales distribuciones se llaman uniformes. Se puede caracterizar
completamente una distribució n uniforme con un solo pará metro, su rango (es
decir, valores mínimo y má ximo). Si bien las distribuciones uniformes son bastante
comunes en los juegos de azar, rara vez ocurren en la naturaleza, ni suelen ser ú tiles
para modelar sistemas complejos creados por el hombre.

Las distribuciones uniformes se pueden generar fá cilmente


llamandorandom.uniform(min, max)que devuelve un nú mero de punto
flotante elegido al azar entreminymáximo.
Capítulo 12. Programas estocásticos, probabilidad y 1

12.3.3 Distribuciones Exponenciales y Geométricas


Distribuciones exponenciales, a diferencia de las distribuciones uniformes,
ocurren con bastante frecuencia. A menudo se utilizan para modelar tiempos
entre llegadas, por ejemplo, de automó viles que ingresan a una autopista o
solicitudes de una pá gina web. Son especialmente importantes porque tienen la
propiedad de no tener memoria.

Considere, por ejemplo, la concentració n de una droga en el cuerpo humano.


Suponga que en cada paso de tiempo cada molécula tiene una probabilidad P de ser
eliminada (es decir, de no estar má s en el cuerpo). El sistema no tiene memoria en el
sentido de que en cada paso de tiempo la probabilidad de que una molé cula se
elimine es independiente de lo que sucedió en tiempos anteriores. En el tiempo t = 0,
la probabilidad de que una molécula individual aú n esté en el cuerpo es 1. En el
tiempo t = 1, la probabilidad de que esa molécula aú n esté en el cuerpo es 1 – P. En el
tiempo t = 2, la probabilidad de esa moleculaestar aú n en el cuerpo es (1 – P)2. Má s
generalmente, en el tiempo t la probabilidad de que una molécula individual haya sobrevivido es
(1 – P)t.

Supongamos que en el tiempo t 0 hayMETRO0 moléculas de la droga. En general, en el momentot, el


número de moléculas será M0 multiplicado por la probabilidad de que un mó dulo individual haya sobrevivido en el
tiempot. La función implementada en la Figura
12.11 traza el nú mero esperado de molé culas restantes frente al tiempo.

Figura 12.11 Aclaramiento exponencial de moléculas


def claro(n, p, pasos):
"""Asume
La llamada n & pasos
claro (1000, ints
0.01, positivos,
1000) produce lapa float
trama de la izquierda.
n: el número inicial de moléculas
p: la probabilidad de que una molécula se borre
pasos: la duración de la simulación"""
numRemaining = [n]
para t en rango
(pasos):numRemaining.append(n*((1
-p)**t))
pylab.plot(numRemaining)
pylab.xlabel('Tiempo')
pylab.ylabel('Moléculas restantes')
pylab.title('Liquidación del
1 Capítulo 12. Programas estocásticos, probabilidad y

Este es un ejemplo de decaimiento exponencial. En la prá ctica, el decaimiento


exponencial a menudo se habla en té rminos de vida media, es decir, el tiempo
esperado requerido para que el valor inicial decaiga en un 50%. También se
puede hablar de la vida media de un solo elemento. Por ejemplo, la vida media de
un solo á tomo radiactivo es el tiempo en el que la probabilidad de que ese á tomo
se haya desintegrado es 0,5. Observe que a medida que aumenta el tiempo, el
nú mero de molé culas restantes se aproxima a cero. Pero nunca llegará allí. Esto
no debe interpretarse como una sugerencia de que queda una fracció n de una
molé cula. Má s bien, debe interpretarse como que, dado que el sistema es
probabilístico, nunca se puede garantizar que todas las molé culas se hayan
eliminado.

¿Qué sucede si hacemos que el eje y sea logarítmico (usando pylab.semilogy)?


Obtenemos la trama de arriba y a la derecha. Los valores en el eje y está n
cambiando exponencialmente rá pido en relació n con los valores en el eje x. Si
hacemos que el propio eje y cambie exponencialmente rá pido, obtenemos una línea
recta. La pendiente de esa línea es la tasa de decaimiento.

Crecimiento exponenciales el inverso del decaimiento exponencial. También se


ve con bastante frecuencia en la naturaleza. El interé s compuesto, el crecimiento
de algas en una piscina y la reacció n en cadena en una bomba ató mica son
ejemplos de crecimiento exponencial.

Las distribuciones exponenciales se pueden generar fá cilmente


llamandorandom.expovariable.

La distribució n geométrica es el aná logo discreto de la distribució n


exponencial.72 Por lo general, se considera que describe el nú mero de intentos
independientes necesarios para lograr un primer é xito (o un primer fracaso).
Imagina, por ejemplo, que tienes un coche de mala muerte
eso comienza solo la mitad del tiempo que
giras la llave. Se podría usar una
distribució n geomé trica para caracterizar
el nú mero esperado de veces que tendría
que intentar arrancar el automó vil antes
de tener é xito. Esto se ilustra con el
histograma de la derecha, que fue
producido por el có digo de la figura 12.12.
El histograma implica que la mayoría de
las veces logrará que el auto funcione en
unos pocos intentos. Por otro lado, el
largo
tail sugiere que, en ocasiones, puede correr el riesgo de agotar la batería antes de
que el automó vil se ponga en marcha.

72Elnombre “distribució n geomé trica” surge de su similitud con una “progresió n


geomé trica”. Una progresió n geomé trica es cualquier secuencia de nú meros en la que
cada nú mero, excepto el primero, se obtiene multiplicando el nú mero anterior por un
nú mero constante distinto de cero. Los Elementos de Euclides prueban varios teoremas
interesantes sobre las progresiones geomé tricas.
Capítulo 12. Programas estocásticos, probabilidad y 1

def inicios exitosos (eventProb, numTrials):


"""Supone que eventProb es un flotante que representa una
probabilidad
de un solo intento con éxito. numTrials a positive int Devuelve
una lista del número de intentos necesarios antes de que
éxito para cada
intento.""" triesBeforeSuccess
= []
para t en el rango
(numTrials):
consecFailures = 0
while random.random() >
eventProb: consecFailures +=
1
intenta antes del éxito.append
(consecFailures) devuelve intentos antes del
éxito

random.seed(0)
probOfSuccess =
0.5
numTrials = 5000
Figura 12.12 Una distribución geométrica

12.3.4 Distribución de Benford


La ley de Benford define una distribució n realmente extrañ a. Sea S un gran
conjunto de enteros decimales. ¿Con qué frecuencia esperaría que cada dígito
apareciera como el primer dígito? La mayoría de nosotros probablemente
adivinaría una novena parte del tiempo. Y cuando las personas inventan
conjuntos de nú meros (p. ej., falsificando datos experimentales o cometiendo
fraude financiero), esto suele ser cierto. Sin embargo, no suele ser cierto para
muchos conjuntos de datos que ocurren naturalmente. En cambio, siguen una
distribució n predicha por la ley de Benford.

Se dice que un conjunto de nú meros decimales satisface la ley de Benford 73si la


probabilidad de que el primer dígito seades consistente conP(d) = log10(1 + 1/d).

Por ejemplo, esta ley predice que la probabilidad de que el primer dígito sea 1 es de
alrededor del 30 %. Sorprendentemente, muchos conjuntos de datos reales
parecen observar esta ley. Es posible demostrar que la sucesió n de Fibonacci, por
ejemplo, la satisface perfectamente. Eso es algo plausible, ya que la secuencia es
generada por una fó rmula. Es menos fá cil entender por qué conjuntos de datos tan
diversos como los có digos de acceso del iPhone, el nú mero de seguidores de
Twitter por usuario, la població n de los países o la distancia de las estrellas a la
Tierra se aproximan mucho a la ley de Benford.74

73La ley lleva el nombre del físico Frank Benford, quien publicó un artículo en 1938 que
mostraba que la ley se basaba en más de 20.000 observaciones extraídas de veinte dominios
diferentes. Sin embargo, fue postulado por primera vez en 1881 por el astró nomo Simon
Newcomb.
74http://testingbenfordslaw.com/
1 Capítulo 12. Programas estocásticos, probabilidad y

12.4 ¿Con qué frecuencia gana el mejor equipo?


Hasta ahora, hemos analizado el uso de mé todos estadísticos para ayudar a
comprender los posibles resultados de los juegos en los que no se pretende que
la habilidad desempeñ e un papel. También es comú n aplicar estos mé todos a
situaciones en las que, presumiblemente, hay alguna habilidad involucrada. Fijar
cuotas en un partido de fú tbol, elegir un candidato político con posibilidades de
ganar, invertir en bolsa, etc.

Casi todos los meses de octubre, dos equipos de las Grandes Ligas de Béisbol de
Estados Unidos se encuentran en algo llamado Serie Mundial. Juegan entre sí
repetidamente hasta que uno de los equipos ha ganado cuatro juegos, y ese equipo
se llama (no del todo apropiado) "el campeó n mundial".

Dejando de lado la cuestió n de si hay motivos para creer que uno de los
participantes en la Serie Mundial es de hecho el mejor equipo del mundo, ¿qué
probabilidades hay de que una contienda que puede durar como má ximo siete
juegos determine cuá l de los dos participantes es mejor?

Claramente, cada añ o un equipo saldrá victorioso. Entonces la pregunta es si


debemos atribuir esa victoria a la habilidad oa la suerte. Para abordar esa
pregunta, podemos usar algo llamado valor p. Los valores P se utilizan para
determinar si un resultado es estadísticamente significativo o no.

Para calcular un valor p se necesitan dos cosas:

 Una hipó tesis nula. Esta hipó tesis describe el resultado que se obtendría si
los resultados se determinaran completamente por casualidad. En este
caso, la hipó tesis nula sería que los equipos tienen el mismo talento, por lo
que si los dos equipos jugaran un nú mero infinito de series de siete juegos,
cada uno ganaría la mitad de las veces.
 Una observació n. Datos recopilados ya sea observando lo que sucede o
ejecutando una simulació n que uno cree que proporciona un modelo
preciso de lo que sucedería.

El valor p nos da la probabilidad de que la observació n sea consistente con la


hipó tesis nula. Cuanto menor sea el valor p, má s probable es que debamos
rechazar la hipó tesis de que la observació n se debe completamente al azar. Por
lo general, insistimos en que p no sea mayor que 0,05 antes de considerar que
un resultado es estadísticamente significativo. Es decir, insistimos en que no hay
má s de un 5% de probabilidad de que se cumpla la hipó tesis nula.

Volviendo a la Serie Mundial, ¿deberíamos considerar que los resultados de esa


serie de siete juegos son estadísticamente significativos? Es decir, ¿deberíamos
concluir que el mejor equipo sí ganó ?

La figura 12.13 contiene có digo que puede proporcionarnos una idea de esa
pregunta. La funció n simSeries tiene un argumento, numSeries, un entero positivo
que describe el nú mero de series de siete juegos que se van a simular. Traza la
probabilidad de que el mejor equipo gane la serie contra la probabilidad de que ese
equipo gane un solo juego. Varía la probabilidad de que el mejor equipo gane un solo
juego de 0,5 a 1,0 y produce una grá fica.
Capítulo 12. Programas estocásticos, probabilidad y 1

def playSeries(numGames, teamProb):


"""Asume numGames un entero
impar,
teamProb un flotador entre 0 y 1
Devuelve verdadero si el mejor equipo
gana la serie"""
número ganado = 0
para juego en rango (numGames):
if random.random() <=
teamProb: numWon += 1
volver (numWon > numGames//2)

def
simSeries(numSeries
): prob = 0.5
fracWon =
[] probs =
[]
while prob <=
1.0:
seriesWon =
0.0
for i in
range(numSeries): if
playSeries(7, prob):
seriesWon += 1
fracWon.append(seriesWon/numSeries)
probs.append(prob)
probabilidad += 0.01
pylab.plot(probs, fracWon, linewidth = 5)
pylab.xlabel('Probabilidad de ganar un juego')
Figura 12.13 Simulación de la Serie Mundial

Cuando simSeries se usa para simular 400


series de siete juegos, produce la trama de
la derecha. Tenga en cuenta que para que el
mejor equipo gane el 95 % de las veces
(0,95 en el eje y), debe ser tres veces mejor
que su oponente. Es decir, el mejor equipo
necesita ganar, en promedio, más de tres
de cuatro juegos (0,75 en el eje x). A modo
de comparació n, en 2009, los dos equipos
de la Serie Mundial tuvieron porcentajes de
victorias en la temporada regular del 63,6
% (Nueva York
Yankees) y 57.4% (Philadelphia Phillies). Esto sugiere que Nueva York debería ganar
alrededor del 52,5% de los juegos entre los dos equipos. Nuestra trama nos dice que
incluso si se enfrentaran en 400 series de siete juegos, los Yankees ganarían menos
del 60% de las veces.

Supongamos que asumimos que estos porcentajes ganadores son reflejos precisos
de las fortalezas relativas de estos dos equipos. ¿Cuá ntos juegos debe durar el
1 Capítulo 12. Programas estocásticos, probabilidad y

Serie Mundial para que obtengamos resultados que nos permitan rechazar la
hipó tesis nula, es decir, la hipó tesis de que los equipos está n parejos?

El có digo de la figura 12.14 simula 200 instancias de series de diferentes


longitudes y traza una aproximació n de la probabilidad de que gane el mejor
equipo.
def Figura 12.14 ¿Cuánto debería durar la Serie Mundial?
findSeriesLength(teamProb
): numSeries = 200
maxLen = 2500
El resultado
paso de
= findSeriesLength
10
sugiere que, en estas circunstancias, la
def fracWon(equipProb, numSeries, seriesLen):
Serie Mundial tendría
ganó que ser de
= 0.0
aproximadamente
para seriesjuegos
1000 mucho
en rango (numSeries):
if playSeries(seriesLen,
antes de que pudié ramos rechazar la
hipó tesis nula y decir teamProb): ganado += 1
con seguridad
retorno ganado/numSeries
que el mejor equipo casi seguro había
ganado.winFrac
Programar= una serie de esta
longitud[]puede
xVals =
presentar algunos
[]
problemas prá cticos.
para seriesLen en el rango (1, maxLen, step):
xVals.append(seriesLen)
winFrac.append(fracWon(teamProb, numSeries,
seriesLen))
pylab.plot(xVals, winFrac, linewidth = 5)
pylab.xlabel('Longitud de la serie')
pylab.ylabel('Probabilidad de serie
ganadora') pylab.title(str(round(teamProb,
4)) +
'Probabilidad de que un mejor equipo gane un
juego') pylab.axhline(0.95) #dibujar una línea horizontal en y
= 0.95
Capítulo 12. Programas estocásticos, probabilidad y 1

12.5 Hashing y colisiones


En la Secció n 10.3 señ alamos que al usar una tabla hash má s grande se podría
reducir la incidencia de colisiones y, por lo tanto, reducir el tiempo esperado para
recuperar un valor. Ahora tenemos las herramientas intelectuales necesarias para
examinar esa compensació n con mayor precisió n.

Primero, obtengamos una formulació n precisa del problema.

1. Asumir:
a. El rango de la funció n hash es1anorte,
b. El nú mero de inserciones esk, y
c. La funció n hash produce una distribució n perfectamente uniforme
de las claves utilizadas en las inserciones, es decir, para todas las
claves,llave, y para nú meros enteros,i, en el rango1anorte, la
probabilidad de queclave hash)esies1/n.
2. ¿Cuá l es la probabilidad de que ocurra al menos una colisió n?

La pregunta es exactamente equivalente a preguntar "dados K enteros generados


aleatoriamente en el rango de 1 a n, ¿cuá l es la probabilidad de que al menos dos de
ellos sean iguales". Si K ≥ n, la probabilidad es claramente 1. Pero, ¿qué sucede
cuando K < n?

Como suele ser el caso, es má s fá cil comenzar respondiendo la pregunta inversa,


"dados K enteros generados aleatoriamente en el rango de 1 a n, ¿cuá l es la
probabilidad de que ninguno de ellos sea igual?"

Cuando insertamos el primer elemento, la probabilidad de no tener una colisió n es


claramente 1. ¿Qué tal la segunda inserció n? Dado que quedan n-1 resultados hash
que no son iguales al resultado del primer hash, n-1 de n opciones no generará n una
colisió n. Entonces, la probabilidad de no tener una colisió n en la segunda inserció n
es𝑛!1, y la probabilidad de no colisionar en ninguno de los dos primeros
𝑛
insercioneses 1 ∗ 𝑛!1. Podemos multiplicar estas probabilidades porque para cada
𝑛
inserció n el valor producido por la funció n hash es independiente de todo lo que le
ha precedido.
elmila probabilidad de no tener una colisió n después de tres inserciones es 1 ∗ 𝑛!1 ∗ 𝑛!2. Y
𝑛!1 𝑛!2
adespues
𝑛!𝐾!1
k insercioneses1 ∗ ∗ ∗…∗ 𝑛𝑛
.
𝑛𝑛𝑛

Para obtener la probabilidad de tener al menos una colisió n, restamos este valor de
1, es decir, la probabilidad es

1 − (𝑛 − 1 ∗ 𝑛 − 2 ∗ …∗)𝑛 − 𝐾 − 1
𝑛𝑛𝑛
Dado el tamañ o de la tabla hash y el nú mero de inserciones esperadas, podemos
usar esta fó rmula para calcular la probabilidad de al menos una colisió n. Si K
fuera razonablemente grande, digamos 10 000, sería un poco tedioso calcular la
probabilidad con lá piz y papel. Eso deja dos opciones, matemá ticas y
programació n. Los matemá ticos han utilizado algunas té cnicas bastante
avanzadas para encontrar una manera de aproximar el valor de esta serie. Pero a
menos que K sea muy grande, es má s fá cil ejecutar algú n có digo para calcular el
valor exacto de la serie:
1 Capítulo 12. Programas estocásticos, probabilidad y

def colisiónProb(n, k):


prob = 1.0
para i en el rango (1, k):
prob = prob * ((n - i)/float(n))
devuelve 1 - prob

Si intentamos colisió nProb(1000, 50) obtenemos una probabilidad de alrededor de


0,71 de que haya al menos una colisió n. Si consideramos 200 inserciones, la
probabilidad de colisió n es casi uno. ¿Te parece un poco alto? Escribamos una
simulació n, Figura 12.15, para estimar la probabilidad de al menos una colisió n, y
veamos si obtenemos resultados similares.

Figura 12.15 Simulando una tabla hash


def simInsertions(numIndices, numInsertions):
"""Asume
Si ejecutamos el cóque numIndices y numInsertions son enteros positivos.
digo
Devuelve 1 si hay colisión; 0 de lo contrario"""
print 'Probabilidad
opciones = rango real de una colisión
(númÍndices) =',posibles
#lista de colisionProb(1000, 50)
print 'Est. utilizados
índices probabilidad de colisión =', findProb(1000, 50, 10000)
= []
print
for'Probabilidad real de colisión =', colisionProb(1000, 200) print
i in range(numInsertions):
'Est. probabilidad
hashVal = de colisión =', findProb(1000, 200, 10000)
random.choice(opciones)
se imprime
si se usa hashVal: #hay un retorno de
Probabilidadcolisión
real de 1una colisión = 0.71226865688
demás:
Est. probabilidad de colisión = 0.7119
Probabilidadusado.append(hashVal)
real de una colisión = 0.999999999478
Est.volver 0
probabilidad de una colisión = 1.0

Los resultados
def de la simulació nnumInserciones,
findProb(númÍndices, son reconfortantemente similares a lo que derivamos
numPruebas):
analíticamente.
colisiones = 0.0
para t en el rango (número de ensayos):
¿Debería la alta probabilidad
colisiones de una colisió n hacernos pensar
+= simInsertions(numIndices, que las tablas hash
numInsertions)
return colisiones/numTrials
tienen que ser enormes para ser ú tiles? No. La probabilidad de que haya al menos
una colisió n nos dice poco sobre el tiempo de bú squeda esperado. El tiempo
esperado para buscar un valor depende de la longitud promedio de las listas que
implementan los cubos que contienen los valores que chocaron. Esto es simplemente
el nú mero de inserciones dividido por el nú mero de cubos.
13 PASEOS ALEATORIOS Y MÁS SOBRE DATOS
VISUALIZACIÓN

En 1827, el botá nico escocé s Robert Brown observó que las partículas de polen
suspendidas en el agua parecían flotar al azar. No tenía una explicació n plausible
para lo que llegó a conocerse como movimiento browniano y no intentó
modelarlo matemá ticamente.75 Un modelo matemá tico claro del fenó meno se
presentó por primera vez en 1900 en la tesis doctoral de Louis Bachelier, La
teoría de la especulació n. Sin embargo, dado que esta tesis abordaba el entonces
desacreditado problema de comprender los mercados financieros, fue ignorada
en gran medida por acadé micos respetables. Cinco añ os má s tarde, un joven
Albert Einstein trajo este tipo de pensamiento estocá stico al mundo de la física
con un modelo matemá tico casi igual al de Bachelier y una descripció n de có mo
podría usarse para confirmar la existencia de los á tomos.76 Por alguna razó n, la
gente parecía pensar que comprender la física era má s importante que ganar
dinero, y el mundo empezó a prestar atenció n. Los tiempos eran ciertamente
diferentes.

El movimiento browniano es un ejemplo de paseo aleatorio. Los paseos aleatorios


se utilizan ampliamente para modelar procesos físicos (p. ej., difusió n), procesos
bioló gicos (p. ej., la ciné tica de desplazamiento del ARN desde heterodú plex por
ADN) y procesos sociales (p. ej., movimientos del mercado de valores).

En este capítulo analizamos los paseos aleatorios por tres razones:

1. Los paseos aleatorios son intrínsecamente interesantes.


2. Nos proporciona un buen ejemplo de có mo usar tipos de datos abstractos y
herencia para estructurar programas en general y simulaciones en
particular.
3. Brinda la oportunidad de presentar algunas características má s de
Python y demostrar algunas té cnicas adicionales para producir grá ficos.

13.1 El paseo del borracho


Veamos una caminata aleatoria que en realidad implica caminar. Un granjero borracho
está parado en medio de un campo, y cada segundo el granjero da un paso en una
direcció n aleatoria. ¿Cuá l es su distancia esperada desde el origen en 1000?

75Tampoco fue el primero en observarlo. Ya en el añ o 60 a. C., el romano Tito Lucrecio, en


su poema “Sobre la naturaleza de las cosas”, describió un fenó meno similar e incluso dio a
entender que era causado por el movimiento aleatorio de los á tomos.
76“Sobre el movimiento de pequeñ as partículas suspendidas en un líquido estacionario
exigido por la teoría cinético-molecular del calor”, Annalen der Physik, mayo de 1905.
Einstein llegaría a describir 1905 como su “annus mirabilis”. Ese añ o, ademá s de su
artículo sobre el movimiento browniano, publicó artículos sobre la producció n y
transformació n de la luz (fundamental para el desarrollo de la teoría cuá ntica), sobre la
electrodiná mica de los cuerpos en movimiento (relatividad especial) y sobre la
equivalencia de materia y energía (E = mc2). No es un mal añ o para un doctorado recié n
acuñ ado.
1 Capítulo 13. Paseos aleatorios y más sobre visualización de

¿segundos? Si da muchos pasos, ¿es probable que se aleje aú n má s del origen, o


es má s probable que regrese al origen una y otra vez y termine no muy lejos de
donde comenzó ? Escribamos una simulació n para averiguarlo.

Antes de comenzar a diseñ ar un programa, siempre es una buena idea tratar de


desarrollar cierta intuició n sobre la situació n que el programa pretende
modelar. Empecemos dibujando un modelo simple de la situació n utilizando
coordenadas cartesianas.
Supongamos que el granjero está parado en un campo donde, misteriosamente,
la hierba ha sido cortada para parecerse a un trozo de papel cuadriculado.
Suponga ademá s que cada paso que da el agricultor tiene una longitud de uno y

es paralelo al eje x o al eje y.

La imagen de la izquierda muestra a un granjero77 de pie en medio del campo. Las


caras sonrientes indican todos los lugares donde podría estar el granjero despué s de
un paso. Note que despué s de un paso ella siempre está exactamente a una unidad
de distancia de donde comenzó . Supongamos que se desplaza hacia el este desde su
ubicació n inicial en su primer paso. ¿Qué tan lejos podría estar de su ubicació n
inicial despué s de su segundo paso? Mirando las caritas sonrientes en la imagen de
la derecha, vemos que con una probabilidad de 0.25 estará a 0 unidades de
distancia, con una probabilidad de 0.25 estará a 2
unidades de distancia, y con una probabilidad de 0.52ellabeunits lejos78. Pronto
promedio ella estará má s lejos despué s de dos pasos que después de un paso. ¿Qué
pasa con el tercer paso? Si el segundo paso es hacia la cara sonriente superior o
inferior, el tercer paso acercará al agricultor al origen la mitad del tiempo y má s lejos
la mitad del tiempo. Si el segundo paso es hacia la carita sonriente izquierda (el
origen), el tercer paso se alejará del origen. Si el segundo paso es hacia la cara
sonriente derecha, el tercer paso estará má s cerca del origen una cuarta parte del
tiempo y má s lejos tres cuartas partes del tiempo.

Parece que cuantos má s pasos da el borracho, mayor es la distancia esperada desde


el origen. Podríamos continuar con esta enumeració n exhaustiva de posibilidades y
quizá s intuir bastante bien có mo crece esta distancia con respecto al nú mero de
pasos. Sin embargo, se está volviendo bastante tedioso, por lo que parece una mejor
idea escribir un programa que lo haga por nosotros.

Comencemos el proceso de diseñ o pensando en algunas abstracciones de datos


que probablemente sean ú tiles para construir esta simulació n y tal vez
simulaciones de otros tipos de paseos aleatorios. Como de costumbre,
deberíamos tratar de inventar tipos que correspondan

77Para ser honesto, la persona que se muestra aquí es un actor profesional que se hace pasar por un
granjero.

78¿Por qué 2? Estamos usando el teorema de Pitá goras.


Capítulo 13. Paseos aleatorios y más sobre la visualización de 1

al tipo de cosas que aparecen en la situació n que estamos tratando de modelar. Tres
tipos obvios son Ubicació n, Campo y Borracho. A medida que observamos las clases
que proporcionan estos tipos, vale la pena pensar en lo que cada una podría
implicar sobre los tipos de modelos de simulació n que nos permitirá n construir.

Empecemos conUbicación.

Figura 13.1Ubicaciónclase
clase Ubicación (objeto):
Esta es una clase simple, pero incorpora dos decisiones importantes. Nos dice
def __init__(self, x, y):
que la simulació n involucrará
"""x e y son como má ximo dos dimensiones. Por ejemplo, la
simulació n no modelará losself.x
flotantes""" cambios= de altitud. Esto es consistente con las
x
imá genes de arriba. Ademá s, dado que los valores de deltaX y deltaY son
self.y = y
flotantes en lugar de nú meros enteros, no existe una suposició n incorporada en
esta clase sobre
def el conjunto
move(self, de direcciones
deltaX, deltaY): en las que podría moverse un
borracho. Esta es una generalizació
"""deltaX y deltaY n del modelo informal en el que cada paso
son
flotantes"""
tenía una longitud de uno y era paralelo al eje x o al eje y.
volver Ubicación(self.x + deltaX, self.y + deltaY)
Class Field también es bastante simple, pero también incorpora decisiones notables.
def getX(self):
Simplemente mantiene un mapeo de borrachos a ubicaciones. No impone
return self.x
restricciones en las ubicaciones, por lo que presumiblemente un campo tiene un
tamañ o ilimitado. Permite agregar mú ltiples borrachos a un campo en ubicaciones
def getY(self):
aleatorias. Noreturn self.y
dice nada sobre los patrones en los que se mueven los borrachos, ni
prohíbe que varios borrachos ocupen el mismo lugar o se muevan a travé s de
def distFrom(yo, otro):
espacios ocupados por otros borrachos.
ox = otro.x
oy = otro.y
xDist = self.x -
ox yDist = self.y
- oy
retorno (xDist**2 + yDist**2)**0.5
1 Capítulo 13. Paseos aleatorios y más sobre visualización de

clase Campo(objeto):

def

__init__(auto):
yo.borrachos = {}

def addDrunk(self, borracho,


loc): si está borracho en
self.drunks:
aumentar ValueError ('Duplicado
borracho') más:
self.drunks[borracho] = loc

def moveDrunk(yo, borracho):


si borracho no en self.drunks:
aumentar ValueError('Borracho no en
el campo') xDist, yDist =
borracho.tomarPaso() ubicaciónActual =
self.borracho[borracho]
#use el método de movimiento de Ubicación para obtener una nueva
ubicación self.drunks[drunk] = currentLocation.move(xDist,
yDist)

Figura 13.2Campoclase

Las clases Drunk y UsualDrunk definen las formas en que un borracho puede
deambular por el campo. En particular, el valor de stepChoices en UsualDrunk
restaura la restricció n de que cada paso tiene una longitud de uno y es paralelo al eje
x o al eje y. Tambié n captura la suposició n de que cada tipo de paso es igualmente
probable y no está influenciado por los pasos anteriores. Un poco má s adelante
veremos las subclases de Drunk con diferentes tipos de comportamientos.

Figura 13.3Ebrioclase base


clase Borracho(objeto):
def __init__(self,
El siguiente paso es usar estasnombre =
clases para construir una simulació n que responda
Ninguno): """Asume que el
la pregunta original. La figura 13.4 contiene tres funciones utilizadas en esta
nombre es una cadena"""
simulació n. self.name
La funció n caminar
= nombresimula una caminata de numSteps pasos. La
funció n simWalks llama a walk para simular numTrials caminatas de numSteps
def una.
pasos cada __str__(self):
La funció n borrachoTest llama a simWalks para simular caminatas
if self !=
de diferentes longitudes.
Ninguno:
return
self.name return
'Anónimo'

clase Borracho
Habitual(Borracho)
Capítulo 13. Paseos aleatorios y más sobre la visualización de 1

El pará metroClase ddeSimPaseoses de tipoclase, y se utiliza en la primera línea de


có digo para crear unEbriode la subclase correspondiente. Má s tarde,
cuandoborracho.tomarpasose invoca desdeField.moveDrunk, el método de la
subclase adecuada se selecciona automá ticamente.

La funció n pruebaborracha tambié n tiene un pará metro, dClase, de tipo clase. Se usa
dos veces, una en la llamada a simWalks y otra en la primera declaració n de impresió n.
En la declaració n de impresió n, el nombre de atributo de clase incorporado se usa para
obtener una cadena con el nombre de la clase. La funció n pruebaEbrio calcula el
coeficiente de variació n de la distancia desde el origen usando la funció n CV definida
en la Figura 12.7.

def caminar(f, d, numPasos):


"""Asume: fa Field, da Drunk in f, y numSteps an int >= 0.
Mueve d numSteps veces y devuelve la diferencia entre
la ubicación final y la ubicación al comienzo de la caminata."""
start = f.getLoc(d)
para s en el rango
(número de pasos):
f.moveDrunk (d)
volver start.distFrom(f.getLoc(d))

def simWalks(númPasos, númPruebas, dClase):


"""Asume numSteps an int >= 0, numTrials an int > 0,
dClass una subclase de Drunk
Simula numTrials paseos de numSteps pasos cada uno.
Devuelve una lista de las distancias finales de cada
prueba"""
Homero = dClase()
origen = Ubicación (0.0,
0.0) distancias = []
para t en el rango (numero
de ensayos): f = campo
()
f.addDrunk(Homer, origen)
distancias.append(walk(f, Homer, numTrials))
distancias de retorno

def pruebaborracho(duracionescaminata, numPruebas,


dClase): """Asume longitudescaminata una
secuencia de enteros >= 0
numTrials an int > 0, dClass una subclase de Drunk
Para cada número de pasos en walkLengths, ejecuta simWalks
con numTrials walks e imprime los resultados"""
para numSteps en walkLengths:
distancias = simWalks(numPasos, numTrials, dClass)
print dClass.__name__, 'caminata aleatoria de', numSteps,
'steps' print ' Mean =', sum(distances)/len(distances),\
'CV =', CV(distancias)
imprimir ' Max =', max(distancias), 'Min =', min(distancias)

Figura 13.4 El paseo del borracho (con un bicho)


1 Capítulo 13. Paseos aleatorios y más sobre visualización de

cuando ejecutamosborrachoPrueba((10, 100, 1000, 10000), 100, Borracho


Habitual), imprimió

UsualDrunk caminata aleatoria de 10 pasos


Media = 9,10300189235 CV = 0,493919383186
Máx. = 23,4093998214 Mín. = 1,41421356237
UsualDrunk caminata aleatoria de 100 pasos
Media = 9,72504983765 CV = 0,583886747239
Máx. = 21,5406592285 Mín. = 0,0
Camino aleatorio borracho habitual de
1000 pasos Media = 9,42444322989 CV =
0,492682758402
Máx. = 21,0237960416 Mín. = 0,0
Caminata aleatoria habitual borracha de
10000 pasos Media = 9.27206514705 CV =
0.540211143752
Máx. = 24,6981780705 Mín. = 0,0

Esto es sorprendente, dada la intuició n que desarrollamos anteriormente de que


la distancia media debería crecer con el nú mero de pasos. Podría significar que
nuestra intuició n está equivocada, o podría significar que nuestra simulació n
tiene errores, o ambas cosas.

Lo primero que debe hacer en este punto es ejecutar la simulació n en valores para
los que ya creemos que sabemos la respuesta, y asegurarse de que lo que produce la
simulació n coincida con el resultado esperado. Probemos caminatas de cero pasos
(para las cuales las distancias media, mínima y má xima desde el origen deben ser
todas 0) y un paso (para las cuales las distancias media, mínima y má xima desde el
origen deben ser todas 1).

cuando corrimosborrachoPrueba((0,1), 100, Borracho Habitual), obtuvimos el


resultado altamente sospechoso
UsualDrunk caminata aleatoria de 0 pasos
Media = 9,10300189235 CV = 0,493919383186
Máx. = 23,4093998214 Mín. = 1,41421356237
UsualDrunk caminata aleatoria de 1 pasos
Media = 9,72504983765 CV = 0,583886747239
Máx. = 21,5406592285 Mín. = 0,0

¿Có mo diablos puede ser má s de 9 la distancia media de una caminata de cero pasos?

Debemos tener al menos un error en nuestra simulació n. Despué s de algunas


investigaciones, el problema es claro. En simWalks, la llamada walk(f, homer,
numTrials) debería haber sido walk(f, homer, numSteps). La moraleja aquí es
importante: siempre muestre algo de escepticismo al mirar los resultados de
una simulació n. Pregunte si los resultados son plausibles y “pruebe con
humo”79 la simulació n en pará metros para los que tiene una fuerte intuició n
acerca de cuá les deberían ser los resultados.

79en el 19elsiglo, se convirtió en una prá ctica está ndar para los plomeros probar los sistemas
cerrados de tuberías para detectar fugas llenando el sistema con humo. Má s tarde, los
ingenieros electró nicos adoptaron el término para cubrir la primera prueba de una pieza
electró nica: encender la energía y buscar humo. Aú n má s tarde, los desarrolladores de
software comenzaron a usar el té rmino para una prueba rá pida para ver si un programa hizo
algo ú til.
Capítulo 13. Paseos aleatorios y más sobre la visualización de 1

Cuando la versió n corregida de la simulació n se ejecuta en nuestros dos casos


simples, arroja exactamente las respuestas esperadas:
Camino aleatorio borracho
habitual de 0 pasos Media = 0.0
CV = nan80
Máx. = 0,0 Mín. = 0,0
Paseo aleatorio borracho habitual
de 1 paso Media = 1,0 CV = 0,0
Máx. = 1,0 Mín. = 1,0

Cuando se ejecuta en caminatas má s largas, se imprime


UsualDrunk caminata aleatoria de 10 pasos
Media = 2,97977767074 CV = 0,497873216438
Máx. = 6,0 Mín. = 0,0
UsualDrunk caminata aleatoria de 100 pasos
Media = 9,34012695549 CV = 0,481221153556
Máx. = 23,4093998214 Mín. = 1,41421356237
Camino aleatorio borracho habitual de
1000 pasos Media = 28,6328252832 CV =
0,510288443239
Máx. = 70,2139587262 Mín. = 3,16227766017
Caminata aleatoria habitual borracha de
10000 pasos Media = 85.9223793386 CV =
0.516182207636
Máx. = 256,007812381 Mín. = 17,7200451467

Como se anticipó , la distancia promedio desde el origen crece con el nú mero de


pasos.

Ahora veamos un grá fico de las distancias medias desde el origen. Para dar una idea
de qué tan rá pido está creciendo la distancia, hemos colocado en la grá fica una línea
que muestra la raíz cuadrada del nú mero de pasos (y aumentamos el nú mero de
pasos a 1,000,000).81

¿Este grá fico proporciona alguna


informació n sobre la ubicació n
final esperada de un borracho?
Sí nos dice que, en promedio, el
borracho estará en algú n lugar
de un círculo con su centro en el
origen y con un radio igual a la
distancia esperada desde el
origen. Sin embargo, nos dice
muy poco acerca de dó nde
podemos encontrar al borracho
al final de una caminata en
particular. Volveremos a este
tema má s adelante en este
capítulo.

80Dadoque la media era cero, el coeficiente de variació n no está definido. Por lo tanto, nuestra
implementació n de CV devolvió el valor especial de punto flotante "no es un nú mero".
81La grá fica que muestra la raíz cuadrada del nú mero de pasos versus la distancia desde el
origen es una línea recta porque usamos una escala logarítmica en ambos ejes.
1 Capítulo 13. Paseos aleatorios y más sobre visualización de

13.2 Paseos aleatorios sesgados


Ahora que tenemos una simulació n funcional, podemos comenzar a modificarla
para investigar otros tipos de paseos aleatorios. Supongamos, por ejemplo, que
queremos considerar el comportamiento de un granjero borracho en el hemisferio
norte que odia el frío, e incluso en su estupor de borracho es capaz de moverse el
doble de rá pido cuando sus movimientos aleatorios lo llevan en direcció n sur. O tal
vez un borracho fototró pico que siempre se mueve hacia el sol (este por la mañ ana
y oeste por la tarde). Todos estos son ejemplos de caminatas aleatorias sesgadas. El
paseo sigue siendo estocá stico, pero hay un sesgo en el resultado.

La figura 13.5 define dos subclases adicionales de Drunk. En cada caso, la


especializació n implica elegir un valor apropiado para stepChoices. La funció n
simAll itera sobre una secuencia de subclases de Drunk para generar informació n
sobre có mo se comporta cada tipo.

class
Figura 13.5 Subclases de la clase base Borracho
ColdDrunk(borrach
cuando o):
corrimos
def simAll((borracho
dar un habitual, borracho frío, borracho EWD),
(100, 1000), 10)imprimió
paso(self):
opciones de paso = [(0.0,1.0), (0.0,-2.0), (1.0, 0.0), (-1.0,
UsualDrunk caminata aleatoria de 100 pasos
0.0)]
Media =return
8,37073251526 CV = 0,482770539323
random.choice(opcionespaso)
Máx. = 14,7648230602 Mín. = 1,41421356237
Caminata aleatoria habitual borracha de
class
1000 pasos Media = 21.0385788624 CV =
EWDrunk(borracho)
0.5489414497
: def dar un
Máx. = 36,6878726557
paso(uno mismo): Mín. = 3,16227766017
ColdDrunk caminata
opciones de aleatoria de 100
paso = [(1.0, pasos
0.0), (-1.0, 0.0)]
Media = 23,9034750714 CV = 0,401318542296
Máx. = 37,1214223865 Mín. = 5,83095189485
ColdDrunk caminata aleatoria de 1000 pasos
Media = 238,833279891 CV = 0,125076661085
Máx. = 288,140590684 Mín. = 182,024723595
EWDrunk caminata aleatoria de
100 pasos Media = 8.6 CV =
0.58879018145
Máx. = 18,0 Mín. = 0,0
EWDrunk paseo aleatorio de 1000
pasos Media = 27,0 CV =
0,726719143346
Máx. = 74,0 Mín. = 2,0
Capítulo 13. Paseos aleatorios y más sobre la visualización de 1

Esto es un poco de salida para digerir. Parece que nuestro borracho que busca
calor se aleja del origen má s rá pido que los otros dos tipos de borracho. Sin
embargo, no es fá cil digerir toda la informació n en este resultado.

Una vez má s, es hora de alejarse de la salida textual y comenzar a usar grá ficos.

Dado que estamos mostrando varios tipos diferentes de borrachos en la misma


trama, asociaremos un estilo distinto con cada tipo de borracho para que sea fá cil
diferenciarlos. El estilo tendrá tres aspectos:

 El color de la línea y los puntos,


 La forma del marcador utilizado para indicar un punto, y
 El estilo de una línea, por ejemplo, só lido o punteado.
La claseiterador de estilo, en la figura 13.6, rota a través de una secuencia de
estilos definidos por el argumento para iterador de estilo. en eso.

Figura 13.6 Iterando sobre estilos


class styleIterator(objeto):
def __init__(self,
El có digoestilos):
de la figura 13.7 es similar en estructura al de la figura 13.4. Las
declaracionesself.índice
de impresió n=en 0 simDrunk y simAll no contribuyen en nada al
resultado de laself.estilos = n allí porque esta simulació n puede tardar bastante
simulació n. Está
estilos
tiempo en completarse, y la impresió n de un mensaje ocasional que indica que se
está progresando puede ser bastante tranquilizador para un usuario que se esté
def nextStyle(self):
preguntando resultado
si el programa realmente está progresando. (Recuerde que stdDev se
= self.styles[self.index]
if self.index
definió en la figura 12.4). == len(self.styles) -
1: self.index = 0
demás:
1 Capítulo 13. Paseos aleatorios y más sobre visualización de

def simDrunk(numTrials, dClass, walkLengths):


distanciasmedias = []
cvDistancias = []
para numSteps en walkLengths:
print 'Comenzando simulación de', numPasos,
'pasos' ensayos = simWalks(numPasos, numTrials,
dClass) mean = sum(ensayos)/float(len(ensayos))
meanDistances.append(media)
cvDistances.append(stdDev(trials) /significar)
retorno (distancias medias, distancias cv)

def simAll(drunkKinds, walkLengths, numTrials):


styleChoice = styleIterator(('b-', 'r:', 'm-.'))
for dClass en borrachoKinds:
curStyle = styleChoice.nextStyle()
print 'Comenzando simulación de', dClass.__name__
significa, cvs = simDrunk(numTrials, dClass,
walkLengths) cvMean = sum(cvs)/float(len(cvs))
pylab.plot(walkLengths, mean, curStyle,
etiqueta = dClase.__nombre__ +
'(CV = ' + str(round(cvMean, 4)) + ')')
pylab.title('Distancia media desde el origen ('
+ str(numTrials) +
'trials)') pylab.xlabel('Número de
pasos') pylab.ylabel('Distancia desde el
origen') pylab.legend(loc = 'mejor')
pylab.semilogx()
pylab.semilogía()

simAll((borracho habitual, borracho frío, borracho EWD),

Figura 13.7 Trazado de las caminatas de diferentes borrachos

El có digo de la figura 13.7 produce el


grá fico de la derecha. El borracho
habitual y el borracho fototró pico
(EWDrunk) parecen alejarse del origen
aproximadamente al mismo ritmo,
pero el borracho que busca calor
(ColdDrunk) parece alejarse mucho
má s rá pido. Esto es interesante dado
que, en promedio, solo se mueve un
25% má s rá pido (da, en promedio,
cinco pasos por cada cuatro que dan
los demá s). Ademá s, los coeficientes de
variació n muestran una gran
dispersió n, pero el grá fico no aclara
por qué .

Construyamos un grá fico diferente, que puede ayudarnos a comprender mejor el


comportamiento de estas tres clases. En lugar de trazar el cambio en la distancia a
lo largo del tiempo para un nú mero creciente de pasos, el có digo de la figura 13.8
traza la distribució n de ubicaciones finales para un solo nú mero de pasos.
Capítulo 13. Paseos aleatorios y más sobre la visualización de 1

def obtenerLocsFinales(númPasos, númPruebas,


dClase): locs = []
d = dClase()
origen = Ubicación (0,
0) para t en el rango
(numTrials):
f = Field()
f.addDrunk(d, origin)
for s in
range(numSteps):
f.MoveDrunk(d)locs.append(f.getLoc(d
))
locomotoras de retorno

def plotLocs(drunkKinds, numSteps, numTrials):


styleChoice = styleIterator(('b+', 'r^', 'mo'))
for dClass en borrachoKinds:
locs = getFinalLocs(númPasos, númPruebas, dClase)
xVals, yVals = [], []
para l en locomotoras:
xVals.append(l.getX(
))
yVals.append(l.getY(
))
meanX = sum(xVals)/float(len(xVals))
meanY = sum(yVals)/float(len(yVals))
curStyle = styleChoice.nextStyle()
pylab.plot(xVals, yVals, curStyle,
etiqueta = dClase.__nombre__ + ' Media
loc. = <'
+ str(meanX) + ', ' + str(meanY) +
'>') pylab.title('Ubicación al final de las caminatas ('
+ str(númPasos) + 'pasos)')

Figura 13.8 Trazado de ubicaciones finales

Lo primero que hace plotLocs es inicializar styleChoice con tres estilos diferentes de
marcadores. Luego usa pylab.plot para colocar un marcador en una ubicació n
correspondiente al final de cada
ensayo. La llamada a pylab.plot
establece el color y la forma del
marcador que se trazará usando los
valores devueltos por el iterador
styleIterator. La llamada
plotLocs ((borracho habitual,
borracho frío, borracho EWD),
100, 200)produce la trama de la
derecha. Lo primero que hay que decir
es que nuestros borrachos parecen
comportarse como se anuncia.
Elborrachotermina en el eje x,
elfrioborrachoparecen progresar
hacia el sur, y
elnormalborrachaparece haber
vagado sin rumbo fijo.
1 Capítulo 13. Paseos aleatorios y más sobre visualización de

Pero, ¿por qué parece haber muchos menos marcadores circulares que
triangulares o marcadores +? Porque muchas de las caminatas de EWDrunk
terminaron en el mismo lugar.
Esto no es sorprendente, dado el
pequeñ o nú mero de posibles
criterios de valoració n.
(200) para el EWDrunk. Ademá s, los
marcadores circulares parecen estar
espaciados de manera bastante
uniforme a lo largo del eje x, lo que es
consistente con el coeficiente de
variació n relativamente alto que
notamos anteriormente.

Todavía no es obvio, al menos para


nosotros, por qué ColdDrunk logra,
en promedio, alejarse mucho má s del
origen que los otros tipos de
borrachos. Tal vez es hora de mirar
no en el punto final promedio de muchas caminatas, sino en el camino seguido por
una sola caminata. El có digo de la figura 13.9 produce el grá fico de la derecha.

Figura 13.9 SeguimientonumPasos):


def traceWalk(tiposborrachos, de caminatas
styleChoice = styleIterator(('b+', 'r^',
Dado que la caminata tienef 200
'mo')) pasos de largo y la caminata del EWDrunk visita
= Field()
menos de 30 lugares diferentes,
para dClass enestá claro que pasa mucho tiempo volviendo sobre
sus pasos. El mismo tipo de observació ndvale
borrachoKinds: = para el UsualDrunk. En contraste,
dClass()
aunque ColdDrunk no se dirige exactamente
f.addDrunk(d, directamente a Florida, se las arregla
Ubicación(0,
para pasar relativamente menos tiempo
0)) locs = [] visitando lugares en los que ya ha estado.
para s en el rango (número
de pasos): f.moveDrunk
(d)locs.append(f.getLoc(
d))
xValores
= []
yValores
= []
para l en locomotoras:
xVals.append(l.getX(
))
yVals.append(l.getY(
))
curStyle = styleChoice.nextStyle()
pylab.plot(xVals, yVals, curStyle,
label =
dClass.__name__) pylab.title('Puntos
Capítulo 13. Paseos aleatorios y más sobre la visualización de 1

Ninguna de estas simulaciones es interesante por derecho propio. (En el pró ximo
capítulo, veremos má s simulaciones intrínsecamente interesantes). Pero hay
algunos puntos que vale la pena mencionar:

 Inicialmente dividimos nuestro có digo de simulació n en cuatro partes


separadas. Tres de ellos eran clases (Ubicación,Campo, yEbrio)
correspondientes a tipos de datos abstractos que aparecían en la descripció n
informal del problema. El cuarto fragmento era un grupo de funciones que
usaban estas clases para realizar una simulació n simple.
 Luego elaboramosEbrioen una jerarquía de clases para que podamos
observar diferentes tipos de caminatas aleatorias sesgadas. El có digo
paraUbicaciónyCampopermaneció intacto, pero el có digo de simulació n se
cambió para iterar a travé s de las diferentes subclases deEbrio. Al hacer
esto, aprovechamos el hecho de que una clase es en sí misma un objeto y,
por lo tanto, se puede pasar como un argumento.
 Finalmente, realizamos una serie de cambios incrementales en la simulació n
que no implicaron ningú n cambio en las clases que representan los tipos
abstractos. Estos cambios consistieron principalmente en la introducció n de
parcelas diseñ adas para dar una idea de los diferentes paseos. Esto es muy
típico de la forma en que se desarrollan las simulaciones. Uno hace que la
simulació n bá sica funcione primero y luego comienza a agregar
características.

13.3 Campos traicioneros


¿Alguna vez jugaste al juego de mesa conocido como Chutes and Ladders en los
EE. UU. y Snakes and Ladders en el Reino Unido? Este juego infantil se originó en
la India (quizá s en el siglo II a. C.), donde se le llamó Moksha-patamu. Aterrizar
en un cuadrado que representaba la virtud (p. ej., la generosidad) enviaba al
jugador a un nivel superior en la vida. Al aterrizar en un cuadrado que
representa el mal (p. ej., lujuria), el jugador regresa a un nivel inferior de vida.

Podemos agregar fá cilmente este tipo de característica a nuestras caminatas


aleatorias creando un campo con agujeros de gusano,82 como se muestra en la
figura 13.10, y reemplazando la segunda línea de có digo en la funció n traceWalk
por la línea de có digo
f = campo impar(1000, 100, 200).

82Estetipo de agujero de gusano es un concepto hipoté tico inventado por físicos teó ricos.
Proporciona atajos a travé s del continuo tiempo/espacio.
1 Capítulo 13. Paseos aleatorios y más sobre visualización de

clase campo impar (campo):


def __init__(self, numHoles, xRange, yRange):
Field.__init__(self)
self.agujeros de gusano = {}
para w en el rango (numero de agujeros):
x = random.randint(-xRange,
xRange) y = random.randint(-
yRange, yRange)
newX = random.randint(-xRange,
xRange) newY = random.randint(-
yRange, yRange) newLoc =
Location(newX, newY)
self.wormholes[(x, y)] = newLoc

def moveDrunk(self, borracho):


Field.moveDrunk(self,
borracho) x =
self.drunks[borracho].getX(
) y =
Figura 13.10 Campos con propiedades extrañas

cuando corrimostraceWalk ((borracho habitual, borracho frío, borracho


EWD), 500), obtuvimos la trama bastante extrañ a

Claramente, cambiar las propiedades del campo ha tenido un efecto dramá tico. Sin
embargo, ese no es el punto de este ejemplo. Los puntos principales son:

 Debido a la forma en que estructuramos nuestro có digo, fue fá cil adaptar un


cambio significativo a la situació n que se estaba modelando. Así como
podríamos agregar diferentes tipos de borrachos sin tocar Campo, podemos
añ adir un nuevo tipo deCamposin tocarEbrioo cualquiera de sus subclases.
(Si hubié ramos sido lo suficientemente previsores para hacer del campo un
pará metro derastrearcaminar, no hubiéramos tenido que
cambiarrastrearcaminarcualquiera.)
 Si bien habría sido factible derivar analíticamente diferentes tipos de
informació n sobre el comportamiento esperado de la caminata aleatoria
simple e incluso de las caminatas aleatorias sesgadas, habría sido un desafío
hacerlo una vez que se introdujeron los agujeros de gusano. Sin embargo,
fue extremadamente simple cambiar la simulació n para modelar la nueva
situació n. Los modelos de simulació n a menudo disfrutan de esta ventaja en
relació n con los modelos analíticos.
14 SIMULACIÓN DEL MONTE CARLO

En los dos capítulos anteriores vimos diferentes formas de usar la aleatoriedad en


los cá lculos. Muchos de los ejemplos que presentamos pertenecen a la clase de
computació n conocida como simulació n Monte Carlo.

Stanislaw Ulam y Nicholas Metropolis acuñ aron el término simulació n Monte Carlo
en 1949 en homenaje a los juegos de azar que se jugaban en el casino del Principado
de Mó naco. Ulam, mejor conocido por diseñ ar la bomba de hidró geno con Edward
Teller, describió la invenció n del modelo de la siguiente manera:
Los primeros pensamientos e intentos que hice para practicar [el Método
Monte Carlo] fueron sugeridos por una pregunta que se me ocurrió en 1946
mientras me recuperaba de una enfermedad y jugaba solitarios. La
pregunta era ¿cuáles son las posibilidades de que un solitario Canfield con
52 cartas salga con éxito? Después de pasar mucho tiempo tratando de
estimarlos mediante cálculos combinatorios puros, me preguntaba si un
método más práctico que el "pensamiento abstracto" no sería exponerlo,
digamos, cien veces y simplemente observar y contar el número de jugadas
exitosas. Esto ya era posible de prever con el comienzo de la nueva era de
las computadoras rápidas,83e inmediatamente pensé en problemas de
difusión de neutrones y otras cuestiones de física matemática, y más
generalmente en cómo cambiar procesos descritos por ciertas ecuaciones
diferenciales en una forma equivalente interpretable como una sucesión de
operaciones aleatorias. Más tarde
… [en 1946,] describí la idea a John von Neumann y comenzamos a
planificar los cálculos reales.84

La técnica se usó con eficacia durante el Proyecto Manhattan para predecir lo que
sucedería durante una reacció n de fisió n nuclear, pero realmente no despegó hasta
la dé cada de 1950, cuando las computadoras se volvieron má s comunes y má s
poderosas.

Ulam no fue el primer matemá tico en pensar en usar las herramientas de


probabilidad para entender un juego de azar. La historia de la probabilidad está
íntimamente relacionada con la historia de los juegos de azar. Es la existencia de
la incertidumbre lo que hace posible el juego. Y la existencia de los juegos de
azar provocó el desarrollo de gran parte de las matemá ticas necesarias para
razonar sobre la incertidumbre. Las contribuciones a los fundamentos de la
teoría de la probabilidad por parte de Cardano, Pascal, Fermat, Bernoulli, de
Moivre y Laplace fueron todas motivadas por el deseo de comprender mejor (y
tal vez beneficiarse de) los juegos de azar.

83"Rá pidoes un termino relativo. Ulam probablemente se estaba refiriendo al ENIAC, que
podría realizar alrededor de 103adiciones un segundo (y pesaba má s de 25 toneladas).
Las computadoras actuales realizan alrededor de 10 9adiciones un segundo (y pesan tal
vez 10-3montones).
84Eckhardt,Roger (1987). "Stan Ulam, John von Neumann y el mé todo Monte Carlo", Los
Alamos Science, edició n especial (15), 131-137.
1 Capítulo 14. Simulación Monte

14.1 El problema de Pascual


La mayor parte del trabajo inicial sobre la teoría de la probabilidad giraba en torno a
los juegos con dados.85 Se dice que el interé s de Pascal en el campo que llegó a
conocerse como teoría de la probabilidad comenzó cuando un amigo le preguntó si
sería rentable o no apostar a que dentro de veinticuatro tiradas de un par de dados
tiraría un doble seis. Esto se consideró un problema difícil a mediados del siglo XVII.
Pascal y Fermat, dos tipos bastante inteligentes, intercambiaron varias cartas sobre
có mo resolver el problema, pero ahora parece una pregunta fá cil de responder:

 En la primera tirada, la probabilidad de sacar un seis en cada dado es 1/6,


entonces la probabilidad de sacar un seis con ambos dados es 1/36.
 Por lo tanto, la probabilidad de no sacar un seis doble en la primera tirada es
1 - 1/36 = 35/36.

 Por lo tanto, la probabilidad de no sacar un doble seis veinticuatro


consecutivosveces es(35/36)24,cerca de0.51,y por lo tanto la probabilidad de sacar un
doble seis es1 - (35/36)24,acerca de0.49.A la larga, no sería rentable apostar a que salga un
doble seis en veinticuatro tiradas.86
Solo para estar seguros, escribamos un pequeñ o programa para simular el juego
del amigo de Pascal y confirmemos que obtenemos la misma respuesta que
Pascal.
def rodarMorir():
Figura 14.1 Comprobación del análisis de Pascal
volver random.choice([1,2,3,4,5,6])

def checkPascal(numTrials):
"""Asume numTrials an int > 0
Imprime
85Las excavaciones una estimación
arqueoló gicas sugierendeque
lalosprobabilidad de ganar"""
dados son el instrumento de juego má s
antiguo numWins
de la raza = 0.0 El dado de seis caras "moderno" má s antiguo que se conoce
humana.
for i indel 600 a. C., pero las tumbas egipcias que datan de dos milenios antes
data de alrededor
del nacimientorange(numTrials):
de Cristo contienen artefactos que se asemejan a dados. Por lo general,
estos primerosfor j in
dados range(24):
estaban hechos de huesos de animales; en los círculos de juego, la
d1frase
gente todavía usa la = "hacer rodar los huesos".
tirarDie()
86Al igual que con nuestros aná lisis anteriores, esto es cierto só lo bajo la suposició n de que
d2 =
cada dado es justo, es decir, el resultado de una tirada es verdaderamente aleatorio y cada
tirarDie()
uno de los seis resultados
si d1 == es 6igualmente
y d2 == probable.
6: Esto no siempre se debe dar por
sentado. Las excavaciones de Pompeya descubrieron dados "cargados" en los que se
habían insertado pequeñ os pesos de plomo para sesgar el resultado de una tirada. Má s
recientemente, el sitio de un vendedor en línea dijo: “¿Tienes una mala suerte inusual
cuando se trata de tirar los dados? Invertir en un par de dados que sean má s confiables
podría ser justo lo que necesita”.
Capítulo 14. Simulación Monte 1

Cuando se ejecuta por primera vez, la llamada chequePascal(1000000)impreso


Probabilidad de ganar = 0.491204

eleses de hecho bastante cerca de1 - (35/36)24;mecanografía1 -(35,0/36,0)**24 enel shell


de Python produce0.49140387613090342.

14.2 ¿Paso o no paso?


No todas las preguntas sobre juegos de azar son tan fá ciles de responder. En el
juego de dados, el tirador (la persona que tira los dados) elige entre hacer una
apuesta de “línea de pase” o de “línea de no pase”.

 Línea de Pase: El tirador gana si la primera tirada (llamada saliendo) es


“natural” (7o11) y pierde si es “craps” (2,3, o12). Si sale algú n otro nú mero,
ese nú mero se convierte en el "punto" y el tirador sigue tirando. Si el tirador
saca el punto antes de sacar un7, el tirador gana. De lo contrario, el tirador
pierde.
 Don't Pass Line: El tirador pierde si la primera tirada es 7o11, gana si es2o3, y
lazos (un "empujó n" en la jerga de los juegos de azar) si es12. Si sale algú n
otro nú mero, ese nú mero se convierte en el punto y el tirador sigue tirando.
Si el tirador saca una7antes de rodar el punto, el tirador gana. De lo
contrario, el tirador pierde.
¿Es una de estas una mejor apuesta que la otra? ¿Es una buena apuesta? Es posible
derivar analíticamente la respuesta a estas preguntas, pero parece má s fá cil (al
menos para nosotros) escribir un programa que simule un juego de dados y ver qué
sucede.

La figura 14.2 contiene el corazó n de tal simulació n. Los valores de las variables de
instancia de una instancia de la clase CrapsGame registran el rendimiento de las
líneas de pase y no pase desde el inicio del juego. Los mé todos de observador
passResults y dpResults devuelven estos valores. El método playHand simula una
“mano”87 de un juego. La mayor parte del có digo en playHand es simplemente una
descripció n algorítmica de las reglas establecidas anteriormente. Observe que hay
un bucle en la clá usula else correspondiente a lo que sucede después de que se
establece un punto. Se sale usando una declaració n de ruptura cuando se lanza un
siete o el punto.

87Una mano comienza cuando el tirador está "saliendo", el té rmino que se usa en los dados
para referirse a una tirada antes de que se establezca un punto. Una mano termina cuando
el tirador ha ganado o perdido su apuesta inicial.
1 Capítulo 14. Simulación Monte

clase
CrapsGame(objeto):
def
__init__(self):
self.passWins, self.passLosses = (0,0)
self.dpWins, self.dpLosses, self.dpPushes =
(0,0,0)

def playHand(self):
tirar = tirarDie() +
tirarDie() si tirar == 7 o
tirar == 11:
self.passWins += 1
self.dppérdidas += 1
elif throw == 2 o throw == 3 o throw == 12:
self.passLosses += 1
if throw == 12:
self.dpEmpuja +=
1
demás:
self.dpGana += 1
demás:
punto =
tirar
mientras es
Verdadero:
tirar = tirarDie() +
tirarDie() if tirar ==
apuntar:
self.passWins += 1
self.dpLosses +=
1 descanso
elif throw == 7:
self.passLosses +=
1
Figura 14.2juego de dadosclase

La figura 14.3 contiene una funció n que utiliza la clase CrapsGame. Su estructura
es típica de muchos programas de simulació n:

1. Ejecuta varios juegos (piense en cada juego como una prueba en nuestras
simulaciones anteriores) y acumula los resultados. Cada juego incluye varias
manos, por lo que hay un bucle anidado.
2. Luego produce y almacena estadísticas para cada juego.
3. Finalmente, produce y genera estadísticas resumidas. En este caso,
imprime el retorno esperado de la inversió n (ROI) o cada tipo de línea de
apuestas y la desviació n está ndar de ese ROI.
El retorno de la inversió n se define mediante la ecuació n

𝑔𝑎i𝑛 ƒ𝑟𝑝
𝑅𝑂𝐼 =𝑜ƒ i𝑛𝑣𝑒𝑠𝑡𝑚𝑒𝑛𝑡
𝑐𝑜𝑠𝑡

Dado que las líneas de pase y de no pase pagan dinero parejo (si apuesta $1 y gana,
gana $1), el ROI es

��
𝑅𝑂𝐼 =
𝑛𝑢𝑚𝑏𝑒𝑟 𝑜ƒ 𝑏𝑒𝑡𝑠
Capítulo 14. Simulación Monte 1

Por ejemplo, si hizo 100 apuestas de línea de pase y ganó la mitad de ellas, su ROI
sería
50 − 50 = 0
100

Si apuesta a la línea de no pase 100 veces y tiene 25 victorias y 5 empates, el ROI


sería
25 − 70 = −45 = −4,5
100100

Tenga en cuenta que en crapsSim usamos xrange en lugar de range en los bucles for en
previsió n de ejecutar simulaciones grandes. Recuerde que en Python 2.7 range(n) crea
una secuencia con n elementos, mientras que xrange(n) genera los valores solo cuando
los necesita el bucle for.

Figura 14.3 Simulación de un juego de dados


def crapsSim(handsPerGame, numGames):
"""Asume
Ejecutemos que simulació
nuestra handsPerGame y numGames
n de dados y veamosson qué
enteros > 0
sucede:88
Juega numGames juegos de manosPerGame manos e imprime los
>>> resultados"""juegos
dadosSim(20, 10) = []
Aprobado: ROI medio = -7,0 % std. desarrollo = 23.6854%
No pase:
#JuegaROI mediode= numGames
juegos 4,0 % Desv estándar = 23,5372 %
for t en
xrange(numGames): c
= CrapsGame()
para i en
88Tenga en cuenta que dado que estos programas incorporan aleatoriedad, no debe
xrange(handsPerGame):
esperar obtener resultados idé nticos si ejecuta el có digo usted mismo. Má s importante
c.playHand()
aú n, no haga ninguna apuesta hasta que haya leído la secció n completa.
juegos.append(c)

#Producir estadísticas para cada


juego pROIPerGame, dpROIPerGame
= [], [] para g en juegos:
victorias, derrotas = g.passResults()
pROIPerGame.append((ganancias -
derrotas)/float(handsPerGame)) victorias, derrotas,
empujones = g.dpResults()
dpROIPerGame.append((ganancias - derrotas)/float(handsPerGame))

#Producir e imprimir estadísticas resumidas


ROI medio = str(round((100.0*sum(pROIPerGame)/numGames), 4)) + '%'
sigma = str(round(100.0*stdDev(pROIPerGame), 4)) + '%'
imprimir 'Aprobado:', 'ROI medio =', ROI medio, 'Std. desarrollo
=', sigma ROI medio =
1 Capítulo 14. Simulación Monte

Parece que sería una buena idea evitar la línea de paso, donde el retorno esperado
de la inversió n es una pérdida del 7%. Pero la línea de no pase parece una muy
buena apuesta. ¿O sí?

Mirando las desviaciones está ndar, parece que quizá s la línea de no pase no sea
una buena apuesta despué s de todo. Recuerde que bajo el supuesto de que la
distribució n es normal, el intervalo de confianza del 95 % está comprendido por
dos desviaciones está ndar a cada lado de la media. Para la línea de no pase, el
intervalo de confianza del 95 % es [4,0 – 2*23,5372, 4,0 + 2*23,5372],
aproximadamente [-43 %, +51 %]. Eso ciertamente no sugiere que apostar a la
línea de no pase sea algo seguro.

Es hora de poner en prá ctica la ley de los grandes nú meros.


>>> dadosSim(10000000, 10)
Aprobado: ROI medio = -1,4216 % std. desarrollo = 0.0322%
No pase: ROI medio = -1,3579 % Desv estándar = 0,0334 %

Ahora podemos estar bastante seguros al asumir que ninguno de estos es una buena
apuesta. Parece que la línea de no pase puede ser un poco menos mala. Sin embargo,
debido a que el intervalo de confianza del 95 % [-1,486, -1,3572] para la línea de
aprobació n se superpone con el de la línea de no aprobació n [-1,4247, -1,2911], no
podemos decir con un 95 % de confianza que la línea de no aprobació n la línea es
una mejor apuesta.

Supongamos que en lugar de aumentar el nú mero de manos por juego, aumentamos


el nú mero de juegos, por ejemplo, llamando a crapsSim(20, 1000000). Como se
muestra a continuació n, la media de los ROI estimados está cerca de los ROI reales.
Sin embargo, las desviaciones está ndar siguen siendo altas, lo que indica que el
resultado de un solo juego de 20 manos es muy incierto.
>>>crapsSim(20, 10000000)
Aprobado: ROI medio = -1,4133 % std. desarrollo = 22.3571%
No pase: ROI medio = -1,3649 % Desv estándar = 22,0446 %

Una de las cosas buenas de las simulaciones es que facilitan la realizació n de


experimentos hipotéticos. Por ejemplo, ¿qué pasaría si un jugador pudiera colarse
en un par de dados de tramposo que favorecieran 5 sobre 2 (5 y 2 está n en los
lados opuestos de un dado)? Para probar esto, todo lo que tenemos que hacer es
reemplazar la implementació n de rollDie por algo como
def rodarMorir():
volver random.choice([1,1,2,3,3,4,4,5,5,5,6,6])

Este cambio relativamente pequeñ o en el dado hace una diferencia dramá tica en las
probabilidades,
>>> dadosSim(1000000, 10)
Aprobado: ROI medio = 6,7066 % std. desarrollo = 0.0208%
No pase: ROI medio = -9,4824 % Desv estándar = 0,02 %

¡No es de extrañ ar que los casinos se tomen muchas molestias para asegurarse de
que los jugadores no introduzcan sus propios dados en el juego!
Capítulo 14. Simulación Monte 1

14.3 Uso de la búsqueda de tablas para mejorar el rendimiento


Es posible que no desee intentar ejecutar crapsSim (100000000, 10) en casa. Se tarda
mucho tiempo en completarse en la mayoría de las computadoras. Eso plantea la
cuestió n de si existe una forma sencilla de acelerar la simulació n.

La complejidad dedadosSimesO(playHand)*handsPerGame*numGames. El tiempo de


ejecución demano de juegodepende del nú mero de veces que se ejecuta el bucle
en él.
En principio, el bucle podría ejecutarse un nú mero ilimitado de veces, ya que no
hay límite en cuanto al tiempo que podría llevar sacar un siete o el punto. En la
prá ctica, por supuesto, tenemos todas las razones para creer que siempre
terminará .

Tenga en cuenta, sin embargo, que el resultado de una llamada a playHand no


depende de cuá ntas veces se ejecute el ciclo, sino solo de qué condició n de salida se
alcanza. Para cada punto posible, se puede calcular fá cilmente la probabilidad de
sacar ese punto antes de sacar un siete. Por ejemplo, con un par de dados se puede
sacar un 4 de tres maneras diferentes: <1, 3>, <3, 1> y <2, 2>; y uno puede sacar un
7 en seis diferentes
maneras: <1, 6>, <6, 1>, <2, 5>, <5, 2>, <3, 4>, y<4, 3>. Por lo tanto, salir del bucle
haciendo rodar un7es el doble de probable que salir del bucle haciendo rodar un 4.

La figura 14.4 contiene una implementació n de playHand que explota este


pensamiento. Calculamos previamente la probabilidad de lograr el punto antes de
sacar un 7 para cada valor posible del punto y almacenamos esos valores en un
diccionario.
Supongamos, por ejemplo, que el punto es 8. El tirador continú a tirando hasta
que tira el punto o tira los dados. Hay cinco formas de sacar un 8 (<6,2>,
<2,6>, <5,3>, <3,5> y <4,4>) y seis formas de sacar un 7. Entonces, el valor de la clave del
diccionario8 es el valor de la expresión 5/11.0. Tener esta tabla nos
permite reemplazar el ciclo interno, que contenía un número
ilimitado de tiradas, con una prueba contra una llamada a
random.random. La complejidad asintótica de esta versión de
playHand esO(1).

La idea de reemplazar el cá lculo por la bú squeda de tablas tiene una amplia


aplicabilidad y se usa con frecuencia cuando la velocidad es un problema. La
bú squeda de tablas es un ejemplo de la idea general de intercambiar tiempo por
espacio. Vimos otro ejemplo de esta té cnica en nuestro aná lisis de hash: cuanto
má s grande es la tabla, menos colisiones y má s rá pida la bú squeda promedio. En
este caso, la mesa es pequeñ a, por lo que el coste del espacio es insignificante.
2 Capítulo 14. Simulación Monte

def playHand(self):
#Una implementación alternativa y más rápida de playHand
pointsDict = {4:1/3.0, 5:2/5.0, 6:5/11.0, 8:5/11.0,
9:2/5.0, 10:1/3.0}
tirar = tirarDie() +
tirarDie() si tirar == 7 o
tirar == 11:
self.passWins += 1
self.dppérdidas += 1
elif throw == 2 o throw == 3 o throw == 12:
self.passLosses += 1
if throw == 12:
self.dpEmpuja +=
1
demás:
self.dpGana += 1
demás:
if random.random() <= pointsDict[throw]: # punto antes de 7
self.passWins += 1
self.dppérdidas += 1
demás:# 7 antes del punto

Figura 14.4 Uso de la búsqueda en tablas para mejorar el


rendimiento

14.4 Encontrar 
Es fá cil ver có mo la simulació n de Monte Carlo es ú til para abordar problemas en
los que el no determinismo juega un papel. Curiosamente, sin embargo, la
simulació n de Monte Carlo (y los algoritmos aleatorios en general) se pueden
utilizar para resolver problemas que no son inherentemente estocá sticos, es
decir, para los que no hay incertidumbre sobre los resultados.

Considere π.

Durante miles de añ os, la gente ha sabido que existe una constante, llamada π (pi)
desde el siglo XVIII, tal que la circunferencia de un círculo es igual a π*diá metro y el
á rea del círculo es igual a π*radio2. Lo que no sabían era el valor de esta constante.

Enmide las primeras estimaciones, 4*(8/9)2 = 3,16, se puede encontrar en el papiro egipcio
Rhind, alrededor de 1650 a. Má s de mil añ os después, el Antiguo Testamento implicaba un
valor diferente para π al dar las especificaciones de uno de los proyectos de construcció n del
rey Salomó n,

E hizo un mar de fundición, de diez codos de un borde al otro: era redondo, y su


altura era de cinco codos: y una línea de treinta codos lo rodeaba alrededor. 89

Resolviendo para π, 10π = 30, entonces π = 3. Quizá s la Biblia simplemente está


equivocada, o quizá s el mar fundido no era perfectamente circular, o quizá s la
circunferencia era

89Biblia King James, 1 Reyes 7.23.


Capítulo 14. Simulación Monte 2

medido desde el exterior de la pared y el diá metro desde el interior, o tal vez es solo
una licencia poética. Dejamos que el lector decida.

Arquímedes de Siracusa (287-212 a. C.) derivó los límites superior e inferior del
valor de π mediante el uso de un polígono de alto grado para aproximarse a una
forma circular.
Utilizando un polígono de 96 lados, concluyó que 223/71 < π < 22/7. Dar límites
superiores e inferiores era un enfoque bastante sofisticado para la é poca. Ademá s,
si tomamos su mejor estimació n como el promedio de sus dos límites, obtenemos
3,1418, un error de alrededor de 0,0002. ¡Nada mal!

Mucho antes de que se inventaran las computadoras, los matemá ticos franceses
Buffon (1707-1788) y Laplace (1749-1827) propusieron usar una simulació n
estocá stica para estimar el valor de π.90 Piensa en inscribir un círculo en un
cuadrado con lados de longitud 2, así que que el radio, r, del círculo es de

longitud 1.
Por la definició n de π, á rea = πr2. Como r es 1, π = á rea. Pero, ¿cuá l es el á rea del
círculo? Buffon sugirió que podía estimar el á rea de un círculo dejando caer una gran
cantidad de agujas (que, segú n é l, seguirían un camino aleatorio al caer) en las
cercanías del cuadrado. La relació n entre el nú mero de agujas con puntas dentro del
cuadrado y el nú mero de agujas con puntas dentro del círculo podría usarse para
estimar el á rea del círculo.

Si las ubicaciones de las agujas son verdaderamente aleatorias, sabemos que,


𝑛𝑒𝑒𝑑𝑙𝑒𝑠 i𝑛 𝑐i𝑟𝑐𝑙𝑒
𝑛𝑒𝑒𝑑𝑙𝑒𝑠 i𝑛 𝑠𝑞𝑢𝑎𝑟𝑒 𝑎𝑟𝑒𝑎 𝑜ƒ 𝑐i𝑟𝑐𝑙𝑒
= 𝑎𝑟𝑒𝑎 𝑜ƒ 𝑠𝑞𝑢𝑎𝑟𝑒
resolviendo el á rea del círculo,

∗ 𝑛𝑒𝑒𝑑𝑙𝑒𝑠 i𝑛 𝑐i𝑟𝑐𝑙𝑒
𝑎𝑟𝑒𝑎 𝑜ƒ 𝑐i𝑟𝑐𝑙𝑒 = 𝑛𝑒𝑒𝑑𝑙𝑒𝑠 i𝑛 𝑠𝑞𝑢𝑎𝑟𝑒

Recuerda que el á rea de un cuadrado de 2 por 2 es 4, entonces,

4 ∗ 𝑛𝑒𝑒𝑑𝑙𝑒𝑠 i𝑛 𝑐i𝑟𝑐𝑙𝑒
𝑎𝑟𝑒𝑎 𝑜ƒ 𝑐i𝑟𝑐𝑙𝑒 = 𝑛𝑒𝑒𝑑𝑙𝑒𝑠 i𝑛 𝑠𝑞𝑢𝑎𝑟𝑒

En general, para estimar el á rea de alguna regió n R

1. Elija una regió n envolvente,mi, tal que el á rea demies fá cil de calcular yRse
encuentra completamente dentromi.
2. Elija un conjunto de puntos aleatorios que se encuentran dentro mi.
3. DejarFsea la fracció n de los puntos que caen dentro R.
4. Multiplica el á rea demiporF.

90Buffonpropuso primero la idea, pero había un error en su formulació n que luego fue
corregido por Laplace.
2 Capítulo 14. Simulación Monte

Si prueba el experimento de Buffon, pronto se dará cuenta de que los lugares donde
aterrizan las agujas no son realmente aleatorios. Ademá s, incluso si pudiera
soltarlas al azar, se necesitaría una gran cantidad de agujas para obtener una
aproximació n de π tan buena como la de la Biblia. Afortunadamente, las
computadoras pueden dejar caer agujas simuladas al azar a un ritmo feroz.

La figura 14.5 contiene un programa que estima π utilizando el método de


Buffon-Laplace. Para simplificar, considera solo aquellas agujas que caen en
el cuadrante superior derecho del cuadrado.

La funció n throwNeedles simula dejar caer una aguja usando primero random.random
para obtener un par de coordenadas cartesianas positivas (valores x e y). Luego usa el
teorema de Pitá goras para calcular la hipotenusa del triá ngulo rectá ngulo con base x y
altura y. Esta es la distancia de la punta de la aguja desde el origen (el centro del
cuadrado). Como el radio del círculo es 1, sabemos que la aguja se encuentra dentro
del círculo si y solo si la distancia desde el origen no es mayor que 1. Usamos este
hecho para contar el nú mero de agujas en el círculo.

La funció nobtenerEstusoslanzar agujaspara encontrar una estimació n de π eliminando


número de agujasagujas y promediando el resultado sobrenúmero de ensayosjuicios

La funció n estPi llama a getEst con un nú mero cada vez mayor de agujas hasta que
getEst devuelve una estimació n que, con una confianza del 95 %, está dentro de la
precisió n del valor real. Lo hace llamando a throwNeedles con un nú mero cada vez
mayor de agujas, hasta que la desviació n está ndar de los resultados de las pruebas
numTrials no sea mayor que precision/2.0. Bajo el supuesto de que los errores se
distribuyen normalmente, esto asegura que el 95% de los valores se encuentran dentro
de la precisió n de la media.
Capítulo 14. Simulación Monte 2

def tirarAgujas(númAgujas):
enCírculo = 0
para Agujas en xrange(1, numNeedles +
1): x = random.random()
y = aleatorio.aleatorio()
si (x*x + y*y)**0.5 <=
1.0: enCirculo += 1
#Contar agujas en un solo cuadrante, así que multiplique
por 4 y devuelva 4*(inCircle/float(numNeedles))

def getEst(númAgujas,
númPruebas): estimaciones =
[]
para t en el rango (número de ensayos):
piGuess =
throwNeedles(numNeedles)
estimaciones.append(piGuess)
sDev = stdDev(estimaciones)
curEst =
sum(estimaciones)/len(estimaciones) print
'Est. = ' + str(ronda(actual, 5)) +\
', Est. desarrollador = ' + str(ronda(sDev, 5))\
+ ', Agujas = ' + str(númAgujas)
return (curEst, sDev)

def estPi(precisión,
numTrials): numNeedles =
1000
sDev = precisión

Figura 14.5 Estimación de π

cuando corrimosestPi(0.01, 100)imprimió


Est. = 3,14844, estándar. desarrollador = 0.04789,
Agujas = 1000 Est. = 3,13918, estándar. desarrollador
= 0.0355, Agujas = 2000 Est. = 3,14108, estándar.
desarrollador = 0.02713, Agujas = 4000 Est. =
3,14143, estándar. desarrollador = 0.0168, Agujas =
8000 Est. = 3,14135, estándar. desarrollador =
0.0137, Agujas = 16000 Est. = 3,14131, estándar.
desarrollador = 0.00848, Agujas = 32000 Est. =
3,14117, estándar. desarrollador = 0.00703, Agujas =
64000 Est. = 3,14159, estándar. desarrollador =
0.00403, Agujas = 128000

Como era de esperar, las desviaciones está ndar disminuyeron monó tonamente a
medida que aumentamos el nú mero de muestras. Al principio, las estimaciones del
valor de π también mejoraron de manera constante. Algunos estaban por encima del
valor real y otros por debajo, pero cada aumento en numNeedles llevó a una
estimació n mejorada. Con 1000 muestras por prueba, la estimació n de la simulació n
ya era mejor que las de la Biblia y el Papiro Rhind.

Curiosamente, la estimació n empeoró cuando el nú mero de agujas pasó de 8.000


a 16.000, ya que 3,14135 está má s lejos del verdadero valor de π que 3,14143. Sin
embargo, si observamos los rangos definidos por una desviació n está ndar alrededor
de cada una de las medias, ambos rangos contienen el verdadero valor de π y el
rango asociado con el tamañ o de muestra má s grande es considerablemente má s
pequeñ o. Aunque la estimació n generada con 16.000 muestras resulta estar má s
alejada del valor real de π, deberíamos tener má s confianza en su precisió n. Esto es
extremadamente importante
2 Capítulo 14. Simulación Monte

noció n. No es suficiente producir una buena respuesta. Tenemos que tener una razó n
vá lida para estar seguros de que, de hecho, es una buena respuesta. Y cuando
dejamos caer un nú mero suficientemente grande de agujas, la pequeñ a desviació n
está ndar nos da razones para estar seguros de que tenemos una respuesta correcta.
¿Bien?

No exactamente. Tener una pequeñ a desviació n está ndar es una condició n


necesaria para tener confianza en la validez del resultado. No es una condició n
suficiente. La noció n de una conclusió n estadísticamente vá lida nunca debe
confundirse con la noció n de una conclusió n correcta.

Cada aná lisis estadístico comienza con un conjunto de suposiciones. La


suposició n clave aquí es que nuestra simulació n es un modelo preciso de la
realidad. Recuerde que el diseñ o de nuestra simulació n de Buffon-Laplace
comenzó con un poco de á lgebra que demostraba có mo podíamos usar la
relació n de dos á reas para encontrar el valor de π. Luego traducimos esta idea en
un có digo que dependía de un poco de geometría y la aleatoriedad de
random.random.

Veamos qué pasa si nos equivocamos en algo. Supongamos, por ejemplo, que
reemplazamos el 4 en la ú ltima línea de throwNeedles por un 2 y nuevamente
ejecutamos estPi (0.01, 100). Esta vez se imprime
Est. = 1,57422, estándar. desarrollador = 0.02394,
Agujas = 1000 Est. = 1,56959, estándar. desarrollador
= 0.01775, Agujas = 2000 Est. = 1,57054, estándar.
desarrollador = 0.01356, Agujas = 4000 Est. =
1,57072, estándar. desarrollador = 0.0084, Agujas =
8000 Est. = 1,57068, estándar. desarrollador =
0.00685, Agujas = 16000 Est. = 1,57066, estándar.
desarrollador = 0.00424, Agujas = 32000

La desviació n está ndar de apenas 32 000 agujas sugiere que deberíamos tener
bastante confianza en la estimació n. Pero, ¿qué significa esto realmente? Significa
que podemos estar razonablemente seguros de que si tuviéramos que extraer má s
muestras de la misma distribució n, obtendríamos un valor similar. No dice nada
acerca de si este valor está o no cerca del valor real de π. Una conclusió n
estadísticamente vá lida no debe confundirse con una conclusió n correcta.

Antes de creer en los resultados de una simulació n, debemos tener confianza


tanto en que nuestro modelo conceptual es correcto como en que hemos
implementado correctamente ese modelo. Siempre que sea posible, se debe
intentar validar los resultados contra la realidad. En este caso, se podría usar
algú n otro medio para calcular una aproximació n al á rea de un círculo (p. ej.,
medidas físicas) y verificar que el valor calculado de π esté al menos en la
vecindad correcta.

14.5 Algunas observaciones finales sobre los modelos de simulación


Durante la mayor parte de la historia de la ciencia, los teó ricos utilizaron técnicas
matemá ticas para construir modelos puramente analíticos que podrían usarse para
predecir el comportamiento de un sistema a partir de un conjunto de pará metros y
condiciones iniciales. Esto condujo al desarrollo de importantes herramientas
matemá ticas que van desde el cá lculo hasta la teoría de la probabilidad. Estas
herramientas ayudaron a los científicos a desarrollar una comprensió n
razonablemente precisa del mundo físico macroscó pico.
Capítulo 14. Simulación Monte 2

A medida que avanzaba el siglo XX, las limitaciones de este enfoque se hicieron cada
vez má s claras. Las razones para esto incluyen:

 Un mayor interés en las ciencias sociales, por ejemplo, la economía,


condujo al deseo de construir buenos modelos de sistemas que no
fueran matemá ticamente tratables.
 A medida que los sistemas a modelar se volvieron cada vez má s
complejos, parecía má s fá cil refinar sucesivamente una serie de
modelos de simulació n que construir modelos analíticos precisos.
 A menudo es má s fá cil extraer resultados intermedios ú tiles de una
simulació n que de un modelo analítico, por ejemplo, para jugar juegos
de "qué pasaría si".
 La disponibilidad de computadoras hizo factible ejecutar simulaciones a
gran escala. Hasta la llegada de la computadora moderna a mediados de
los añ os 20elsiglo, la utilidad de la simulació n estaba limitada por el
tiempo requerido para realizar cá lculos a mano.
La simulació n intenta construir un dispositivo experimental, llamado modelo, que
proporcionará informació n ú til sobre los posibles comportamientos del sistema que
se está modelando. Es importante recordar que estos modelos, como todos los
modelos, son solo una aproximació n a la realidad. Nunca se puede estar seguro de
que el sistema real se comportará de la forma predicha por el modelo. De hecho,
normalmente se puede estar bastante seguro de que el sistema real no se
comportará exactamente como predice el modelo. Es una perogrullada
comú nmente citada que “todos los modelos son erró neos, pero algunos son
ú tiles.”91

Los modelos de simulació n son descriptivos, no prescriptivos. Dicen có mo


funciona un sistema en determinadas condiciones; no có mo organizar las
condiciones para que el sistema funcione mejor. Una simulació n no optimiza,
simplemente describe. Eso no quiere decir que la simulació n no se pueda utilizar
como parte de un proceso de optimizació n. Por ejemplo, la simulació n se utiliza
a menudo como parte de un proceso de bú squeda para encontrar un conjunto
ó ptimo de ajustes de pará metros.

Los modelos de simulació n se pueden clasificar en tres dimensiones:

 Determinista versus estocá stico,


 Está tico versus diná mico, y
 Discreta versus continua.

El comportamiento de una simulació n determinista está completamente definido


por el modelo. Volver a ejecutar una simulació n no cambiará el resultado. Las
simulaciones deterministas se utilizan normalmente cuando el sistema que se
modela es demasiado complejo para analizarlo analíticamente, por ejemplo, el
rendimiento de un chip de procesador. Las simulaciones estocá sticas incorporan
aleatoriedad en el modelo. Mú ltiples ejecuciones del mismo modelo pueden
generar diferentes valores. Este elemento aleatorio nos obliga a generar muchos
resultados para ver el abanico de posibilidades. La cuestió n de generar 10, 1000
o 100 000 resultados es una cuestió n estadística, como se discutió anteriormente.

91Suele atribuirse al estadístico George EP Box.


2 Capítulo 14. Simulación Monte

En un modelo está tico, el tiempo no juega un papel esencial. La simulació n de caída


de aguja utilizada para estimar π en este capítulo es un ejemplo de simulació n
está tica. En un modelo diná mico, el tiempo, o algú n aná logo, juega un papel
fundamental. En la serie de caminatas aleatorias simuladas en el Capítulo 13, el
nú mero de pasos dados se utilizó como sustituto del tiempo.

En un modelo discreto, los valores de las variables pertinentes son enumerables, por
ejemplo, son nú meros enteros. En un modelo continuo, los valores de las variables
pertinentes oscilan entre conjuntos no enumerables, por ejemplo, los nú meros
reales. Imagine analizar el flujo de trá fico a lo largo de una carretera. Podríamos
optar por modelar cada automó vil individual, en cuyo caso tenemos un modelo
discreto. Alternativamente, podríamos optar por tratar el trá fico como un flujo,
donde los cambios en el flujo pueden describirse mediante ecuaciones diferenciales.
Esto conduce a un modelo continuo. En este ejemplo, el modelo discreto se parece
má s a la situació n física (nadie conduce medio automó vil, aunque algunos
automó viles tienen la mitad del tamañ o de otros), pero es má s complejo desde el
punto de vista computacional que uno continuo. En la prá ctica, los modelos suelen
tener componentes discretos y continuos. Por ejemplo,
15 ENTENDIENDO LOS DATOS EXPERIMENTALES

Este capítulo tiene que ver con la comprensió n de los datos experimentales.
Haremos un uso extensivo de la representació n grá fica para visualizar los datos
y volveremos al tema de qué es y qué no es una conclusió n estadística vá lida.
Tambié n hablaremos sobre la interacció n entre los experimentos físicos y
computacionales.

15.1 El comportamiento de los resortes


Los resortes son cosas maravillosas. Cuando son comprimidos o estirados por
alguna fuerza, almacenan energía. Cuando ya no se aplica esa fuerza, liberan la
energía almacenada. Esta propiedad les permite suavizar el viaje en los
automó viles, ayudar a que los colchones se ajusten a nuestros cuerpos, retraer los
cinturones de seguridad y lanzar proyectiles.

En 1676, el físico britá nico Robert Hooke formuló la ley de elasticidad de Hooke:
Ut tensio, sic vis, en inglé s, F = -kx. En otras palabras, la fuerza, F, almacenada en
un resorte está linealmente relacionada con la distancia, x, que el resorte ha sido
comprimido (o estirado). (El signo menos indica que la fuerza ejercida por el
resorte tiene la direcció n opuesta al desplazamiento). La ley de Hooke se cumple
para una amplia variedad de materiales y sistemas, incluidos muchos sistemas
bioló gicos. Por supuesto, no es vá lido para una fuerza arbitrariamente grande.
Todos los resortes tienen un límite elá stico, má s allá del cual falla la ley. Aquellos
de ustedes que han estirado demasiado un Slinky saben esto muy bien.

La constante de proporcionalidad, k, se llama constante de resorte. Si el resorte es


rígido (como los de la suspensió n de un automó vil o las extremidades del arco de
un arquero), k es grande. Si el resorte es débil, como el resorte de un bolígrafo, k es
pequeñ o.

Conocer la constante de resorte de un resorte en particular puede ser un asunto de


cierta importancia. Las calibraciones de escalas simples y microscopios de fuerza
ató mica dependen de conocer las constantes de resorte de los componentes. El
comportamiento mecá nico de una hebra de ADN está relacionado con la fuerza
requerida para comprimirla. La fuerza con la que un arco lanza una flecha está
determinada por la constante elá stica de sus extremidades. Etcé tera.
2 Capítulo 15. Comprensión de los datos

Generaciones de estudiantes de física han aprendido a estimar las constantes de los


resortes utilizando un aparato experimental similar al que se muestra aquí. La idea
bá sica es estimar la fuerza almacenada en el resorte midiendo el desplazamiento
causado al ejercer una fuerza conocida sobre el resorte.

Comenzamos con un resorte sin peso y medimos la distancia hasta la parte inferior
del resorte desde la parte superior del soporte. Luego colgamos una masa conocida
en el resorte, esperamos a que deje de moverse y nuevamente medimos la
distancia desde la parte inferior del resorte hasta la parte superior del soporte. La
diferencia entre las dos distancias se convierte entonces en el valor de x en la ley de
Hooke.

Sabemos que la fuerza, F, que se ejerce sobre el resorte es igual a la masa, m,


multiplicada por la aceleració n de la gravedad, g (9,81 m/s2 es una buena
aproximació n de g en este planeta), por lo que sustituimos m*g para F. Por á lgebra
simple sabemos que k = -(m*g)/x.

Supongamos, por ejemplo, quem = 1 kgyx = 0,1 m, entonces


1𝑘𝑔 ∗ 9.81𝑚/𝑠2 9.81𝑁
𝑘= = − 0.1𝑚 = −98.1𝑁/𝑚
0.1𝑚

Segú n este cá lculo, se necesitará n 98,1 Newtons92 de fuerza para estirar el resorte
un metro.

Todo esto estaría muy bien si

 Teníamos plena confianza en nuestra capacidad para realizar este


experimento a la perfecció n. En ese caso, podríamos tomar una medida,
realizar el cá lculo y saber que habíamos encontradok.
Desafortunadamente, la ciencia experimental casi nunca funciona de esta
manera, y

 Podíamos estar seguros de que está bamos operando por debajo del
límite elá stico del resorte.

Un experimento má s robusto es colgar una serie de pesos cada vez má s pesados en


el resorte, medir el estiramiento del resorte cada vez y graficar los resultados.

92El Newton, escrito N, es la unidad internacional está ndar para medir la fuerza. Es la cantidad
de fuerza necesaria para acelerar una masa de un kilogramo a razó n de un metro por segundo
por segundo. Un Slinky, por cierto, tiene una constante de resorte de aproximadamente 1N/m.
Capítulo 15. Comprensión de los datos 2

Ejecutamos un experimento de este tipo y escribimos los resultados en un archivo


llamado
springData.txt:

Distancia (m) Masa (kg)


0.0865 0.1
0.1015 0.15

0.4416 0.9
0.4304 0,95
0,437 1,0

La funció n de la figura 15.1 lee datos de un archivo como el que guardamos y


devuelve listas que contienen las distancias y las masas.

Figura 15.1 Extrayendo los datos de un archivo


def getData(nombreArchivo):
archivo de datos = abrir
La funció n de la figura
(nombre15.2
deusa getData 'r')
archivo, para extraer los datos experimentales del
archivo y luego los representa.
distancias = []
masas = []
descartarHeader =
Figura 15.2 Trazado
dataFile.readline() de los
para línea en datos
def plotData(archivo de entrada):
dataFile:
masas, distancias =
d, m = linea.split('
getData(inputFile) masas =
')distancias.append(float
pylab.array(masas) distancias =
(d))
pylab.array(distancias) fuerzas =
masas*9.81 pylab.plot(fuerzas,
distancias, 'bo',
label = 'Desplazamientos medidos')
pylab.title('Desplazamiento medido del resorte')
pylab.xlabel('|Fuerza| (Newtons)')
pylab.ylabel('Distancia (metros)')
2 Capítulo 15. Comprensión de los datos

CuandoplotData('springData.txt')
se ejecuta, produce el grá fico de la
izquierda.

Esto no es lo que predice la ley de


Hooke. La ley de Hooke nos dice que
la distancia debe aumentar
linealmente con la masa, es decir, los
puntos deben estar en una línea recta
cuya pendiente está determinada por
la constante del resorte. Por
supuesto, sabemos que cuando
tomamos medidas reales, el
los datos experimentales rara vez son una combinació n perfecta para la teoría.
Es de esperar un error de medició n, por lo que deberíamos esperar que los
puntos se encuentren alrededor de una línea en lugar de sobre ella.

Aú n así, sería bueno ver una línea que represente nuestra mejor suposició n de
dó nde habrían estado los puntos si no hubiéramos tenido un error de medició n. La
forma habitual de hacer esto es ajustar una línea a los datos.

15.1.1 Uso de la regresión lineal para encontrar un ajuste


Cada vez que ajustamos cualquier curva (incluida una línea) a los datos,
necesitamos alguna forma de decidir qué curva se ajusta mejor a los datos. Esto
significa que necesitamos definir una funció n objetivo que proporcione una
evaluació n cuantitativa de qué tan bien se ajusta la curva a los datos. Una vez que
tenemos tal funció n, encontrar el mejor ajuste puede formularse como encontrar
una curva que minimice (o maximice) el valor de esa funció n, es decir, como un
problema de optimizació n (véanse los capítulos 17 y 18).

La funció n objetivo má s utilizada se llama mínimos cuadrados. Deje que observado y


predicho sean vectores de igual longitud, donde observado contiene los puntos
medidos y predicho los puntos de datos correspondientes en el ajuste propuesto.

La funció n objetivo se define entonces como:


𝑙𝑒𝑛𝑜𝑏𝑠𝑒𝑟𝑣𝑒𝑑 !1

(𝑜𝑏𝑠𝑒𝑟𝑣𝑒𝑑 − 𝑝𝑟𝑒𝑑i𝑐𝑡𝑒𝑑 yo )2
i!0 yo
Elevar al cuadrado la diferencia entre los puntos observados y predichos hace que
las grandes diferencias entre los puntos observados y predichos sean
relativamente má s importantes que las pequeñ as diferencias. Al elevar al cuadrado
la diferencia, tambié n se descarta informació n sobre si la diferencia es positiva o
negativa.

¿Có mo podríamos encontrar el mejor ajuste por mínimos cuadrados? Una forma de
hacer esto sería usar un algoritmo de aproximació n sucesiva similar al algoritmo de
Newton-Raphson del capítulo 3. Como alternativa, existe una solució n analítica que
suele ser aplicable. Pero no tenemos que usar ninguno de los dos, porque PyLab
proporciona una funció n integrada, polyfit, que encuentra el mejor ajuste por
mínimos cuadrados.
Capítulo 15. Comprensión de los datos 2

La llamada
pylab.polyfit(valoresXobservados, valoresYobservados, n)

encuentra los coeficientes de un polinomio de grado n que proporciona el mejor


ajuste de mínimos cuadrados para el conjunto de puntos definidos por las matrices
observadoXVals y observadoYVals. Por ejemplo, la llamada
pylab.polyfit(valoresXobservados, valoresYobservados, 1)

encontrará una recta descrita por el polinomio y = ax + b, donde a es la pendiente


de la recta y b la intersecció n con el eje y. En este caso, la llamada devuelve una
matriz con dos valores de coma flotante. De manera similar, una pará bola se
describe mediante la ecuació n cuadrá tica y = ax2 + bx + c. Por lo tanto, la llamada
pylab.polyfit(valoresXobservados, valoresYobservados, 2)

devuelve una matriz con tres valores de coma flotante.

El algoritmo utilizado por polyfit se llama regresió n lineal. Esto puede parecer un
poco confuso, ya que podemos usarlo para ajustar curvas que no sean líneas. El
mé todo es lineal en el sentido de que el valor de la variable dependiente es una
funció n lineal de las variables independientes y los coeficientes encontrados por la
regresió n. Por ejemplo, cuando ajustamos una cuadrá tica, obtenemos un modelo de
la forma y = ax2 + bx + c. En tal modelo, el valor de la variable dependiente y es lineal
en las variables independientes x2, x1 y x0 y los coeficientes a, b y c. 93

La funció ndatos de ajusteen la Figura 15.3 extiende elplotDatafunció n en la figura


15.2 agregando una línea que represente el mejor ajuste para los datos.
Usapolifitpara encontrar los coeficientes ayb, y luego usa esos coeficientes para
generar el desplazamiento del resorte previsto para cada fuerza. Note que hay una
asimetría en la formaefectivoydistanciason tratados. Los valores enefectivo(que
se derivan de la masa suspendida del resorte) se tratan como independientes y se
utilizan para producir los valores en la variable dependiente distancias
predichas(una predicció n de los desplazamientos producidos al suspender la masa).

La funció n tambié n calcula la constante de resorte, k. La pendiente de la recta, a, es


∆distancia/∆fuerza. La constante del resorte, por otro lado, es ∆fuerza/∆distancia. Como
consecuenciakes el inverso dea.

93Una funció n es lineal si las variables aparecen solo en primer grado, se multiplican por
constantes y se combinan mediante sumas y restas.
2 Capítulo 15. Comprensión de los datos

def fitData(archivo de entrada):


masas, distancias = getData(inputFile)
distancias = pylab.array(distancias)
masas = pylab.array(masas)
fuerzas = masas*9.81
pylab.plot(fuerzas, distancias,
'bo',
label = 'Desplazamientos medidos')
pylab.title('Desplazamiento medido del
resorte') pylab.xlabel('|Fuerza| (Newtons)')
pylab.ylabel('Distancia (metros)')
#encontrar ajuste lineal
a,b = pylab.polyfit(fuerzas, distancias, 1)
predichasDistancias = a*pylab.array(fuerzas)
+ bk = 1.0/a
pylab.plot(fuerzas, distancias predichas,
label = 'Desplazamientos predichos por\najuste lineal, k
= '
+ str(ronda(k,
Figura 15.3 Ajuste de una curva a los datos

La llamada
fitData('springData.txt')
produce el gráfico de la
derecha. Es interesante
observar que muy pocos
puntos se encuentran
realmente en el ajuste de
mínimos cuadrados. Esto es
plausible porque estamos
tratando de minimizar la
suma de los errores al
cuadrado, en lugar de
maximizar la cantidad de
puntos que se encuentran en
la línea. Aún así, no
parece un gran ajuste.
Probemos un ajuste cúbico
sumando a
datos de ajuste

#encontrar ajuste cúbico


a,b,c,d = pylab.polyfit(fuerzas, distancias, 3) distancias
predichas = a*(fuerzas**3) + b*fuerzas**2 + c*fuerzas + d
pylab.plot(fuerzas, distancias predichas, 'b:', etiqueta = 'ajuste cúbico')

Esto produce la trama de la


izquierda. El ajuste cú bico
parece un modelo mucho
mejor de los datos, pero ¿lo
es?
Probablemente no.

En la literatura té cnica,
con frecuencia se ven
diagramas como este que
incluyen tanto datos sin
procesar como un ajuste
de curva a los datos. Con
demasiada frecuencia, sin
embargo, los autores
Capítulo 15. Comprensión de los datos 2
luego
2 Capítulo 15. Comprensión de los datos

continú e suponiendo que la curva ajustada es la descripció n de la situació n real, y


los datos sin procesar son simplemente una indicació n del error experimental. Esto
puede ser peligroso.

Recuerde que comenzamos con una teoría de que debería haber una relació n lineal
entre los valores de x e y, no uno
cú bico. Veamos qué sucede si
usamos nuestro ajuste cú bico
para predecir dó nde estaría el
punto correspondiente a 1,5 kg.
El resultado se muestra en el
grá fico de la izquierda.

Ahora el ajuste cú bico no se ve


tan bien. En particular, parece
muy poco probable que al
colgar un gran peso en el
resorte podamos hacer que el
resorte se eleve por encima (el
valor de y es negativo)
la barra de la que está suspendido. Lo que tenemos es un ejemplo de overfitting.
El sobreajuste suele ocurrir cuando un modelo es excesivamente complejo, por
ejemplo, tiene demasiados pará metros en relació n con la cantidad de datos.
Cuando esto sucede, el ajuste puede capturar ruido en los datos en lugar de
relaciones significativas. Un modelo que ha sido sobreajustado por lo general
tiene un poder predictivo pobre, como se ve en este ejemplo.

Ejercicio de dedos:Modifique el có digo de la figura 15.3 para que produzca el


grá fico anterior.

Volvamos al ajuste lineal. Por el momento, olvídese de la línea y estudie los


datos en bruto. ¿Algo al respecto parece
¿extrañ o? Si tuviéramos que ajustar
una línea a los seis puntos má s a la
derecha, sería casi paralela al eje x.
Esto parece contradecir la ley de
Hooke, hasta que recordemos que la
ley de Hooke se cumple solo hasta
cierto límite elá stico. Quizá s ese límite
se alcance para esta primavera en
algú n lugar alrededor de 7N
(aproximadamente 0,7 kg). Veamos
qué pasa si eliminamos los ú ltimos
seis puntos reemplazando la segunda
y tercera línea de fitData por
distancias = pylab.array(distancias[:-6])
masas = pylab.array(masas[:-6])

La eliminació n de esos puntos ciertamente marca la diferencia, por ejemplo, k se


ha reducido drá sticamente y los ajustes lineales y cú bicos son casi
indistinguibles. Pero, ¿có mo sabemos cuá l de los dos ajustes lineales es una
mejor representació n de có mo funciona nuestro resorte hasta su límite elá stico?
Capítulo 15. Comprensión de los datos 2
Podríamos usar alguna prueba estadística para
2 Capítulo 15. Comprensión de los datos

determinar qué línea se ajusta mejor a los datos, pero eso no viene al caso. Esta no es
una pregunta que pueda responderse con estadísticas. Después de todo, podríamos
descartar todos los datos excepto dos puntos cualesquiera y saber que polyfit
encontraría una línea que se ajustaría perfectamente a esos dos puntos. Nunca se
deben descartar resultados experimentales simplemente para obtener un mejor
ajuste.94 Aquí justificamos descartar los puntos má s a la derecha apelando a la
teoría subyacente a la ley de Hooke, es decir, que los resortes tienen un límite
elá stico. Esa justificació n no se podría haber utilizado adecuadamente para eliminar
puntos en otras partes de los datos.

15.2 El comportamiento de los proyectiles


Al aburrirnos de simplemente estirar los resortes, decidimos usar uno de nuestros
resortes para construir un dispositivo capaz de lanzar un proyectil.95 Usamos el
dispositivo cuatro veces para disparar un proyectil a un objetivo a 30 yardas (1080
pulgadas) del punto de lanzamiento. Cada vez, medimos la altura del proyectil a
varias distancias desde el punto de lanzamiento. El punto de lanzamiento y el
objetivo estaban a la misma altura, que tratamos como 0,0 en nuestras mediciones.
Los datos se almacenaron en un archivo con el contenido
Distanci juicio1 ensayo2 juicio3 juicio
a 4
1080 0.0 0.0 0.0 0.0
1044 2.25 3.25 4.5 6.5
1008 5.25 6.5 6.5 8.75
972 7.5 7.75 8.25 9.25
936 8.75 9.25 9.5 10.5
900 12.0 12.25 12.5 14.75
864 13.75 16.0 16.0 16.5
828 14.75 15.25 15.5 17.5
792 15.5 16.0 16.6 16.75
756 17.0 17.0 17.5 19.25
720 17.5 18.5 18.5 19.0
540 19.5 20.0 20.25 20.5
360 18.5 18.5 19.0 19.0
180 13.0 13.0 13.0 13.0
0 0.0 0.0 0.0 0.0

La primera columna contiene las distancias del proyectil desde el objetivo. Las
otras columnas contienen la altura del proyectil a esa distancia para cada una de
las cuatro pruebas. Todas las medidas está n en pulgadas.

El có digo de la figura 15.4 se usó para trazar la altitud media del proyectil frente a
la distancia desde el punto de lanzamiento. También traza los mejores ajustes
lineales y cuadrá ticos a los puntos. (En caso de que haya olvidado el significado de
multiplicar una lista por un nú mero entero, la expresió n [0]*len(distancias)
produce una lista de len(distancias) 0).

94Lo cual no quiere decir que la gente nunca lo haga.


95Un proyectil es un objeto que es impulsado a travé s del espacio por el ejercicio de una
fuerza que se detiene despué s de que se lanza el proyectil. En interé s de la seguridad
pú blica, no describiremos el dispositivo de lanzamiento utilizado en este experimento.
Baste decir que fue increíble.
Capítulo 15. Comprensión de los datos 2

def getTrajectoryData(fileName):
dataFile = open(fileName, 'r')
distancias = []
alturas1, alturas2, alturas3, alturas4 = [],[],[],[] descartar
encabezado = archivo de datos.readline()
para línea en archivo de datos:
d, h1, h2, h3, h4 = line.split()
distancias.append(float(d))
heights1.append(float(h1))
heights2.append(float(h2))
heights3.append(float(h3) )
alturas4.append(float(h4))
archivo de datos.cerrar()
volver (distancias, [alturas1, alturas2, alturas3, alturas4])

def trayectorias de proceso (nombre de archivo):


distancias, alturas = getTrajectoryData(fileName)
numTrials = len(alturas)
distancias = pylab.array(distancias)
#Obtenga una matriz que contenga la altura media en cada
distancia a las Alturas =
pylab.array([0]*len(distancias))
para h en alturas:
totHeights = totHeights + pylab.array(h)
meanHeights = totHeights/len(heights)
pylab.title('Trayectoria del proyectil (media de '\
+ str(numTrials) + ' Trials)')
pylab.xlabel('Pulgadas desde el punto de
lanzamiento') pylab.ylabel('Pulgadas por
encima del punto de lanzamiento')
pylab.plot(distancias, alturas medias,
'bo')
a,b = pylab.polyfit(distancias, alturas medias,
1) altitudes = a*distancias + b
pylab.plot(distancias, altitudes, 'b', etiqueta = 'Ajuste
lineal') a,b,c = pylab.polyfit(distancias, alturas medias, 2)
altitudes = a*(distancias**2) + b*distancias + c
pylab.plot(distancias, altitudes, 'b:', label = 'Ajuste
cuadrático') pylab.legend()

Figura 15.4 Trazado de la trayectoria de un proyectil

Una mirada rá pida al diagrama 96 de la


derecha deja bastante claro que un ajuste
cuadrá tico es mucho mejor que uno lineal.
(La razó n por la que el ajuste cuadrá tico
no es suave es que solo estamos trazando
las alturas predichas que corresponden a
las alturas medidas). Pero, ¿qué tan mal
ajuste es la línea y qué tan bueno es el
ajuste cuadrá tico?

96No se deje engañ ar por este complot pensando que el proyectil tenía un á ngulo de ascenso
pronunciado. Solo se ve así debido a la diferencia de escala entre los ejes vertical y horizontal
en el grá fico.
2 Capítulo 15. Comprensión de los datos

15.2.1 Coeficiente de determinación


Cuando ajustamos una curva a un conjunto de datos, encontramos una funció n que
relaciona una variable independiente (pulgadas horizontalmente desde el punto de
inicio en este ejemplo) con un valor predicho de una variable dependiente (pulgadas
por encima del punto de inicio en este ejemplo) . Preguntar sobre la bondad de un
ajuste es equivalente a preguntar sobre la precisió n de estas predicciones. Recuerde
que los ajustes se encontraron minimizando el error cuadrá tico medio. Esto sugiere
que se podría evaluar la bondad de un ajuste observando el error cuadrá tico medio.
El problema con ese enfoque es que si bien existe un límite inferior para el error
cuadrá tico medio (cero), no existe un límite superior. Esto significa que, si bien el
error cuadrá tico medio es ú til para comparar la bondad relativa de dos ajustes con
los mismos datos, no es particularmente ú til para tener una idea de la bondad
absoluta de un ajuste.

Podemos calcular la bondad absoluta de un ajuste usando el coeficiente de


determinació n,a menudo escrito como R2.97 ¡Sea 𝑦i la i𝑡! valor observado, 𝑝i el valor
correspondiente predicho por el modelo y 𝜇 la media de los valores observados.

𝑅2 = 1 − i(𝑦i − 𝑝i)2
i(𝑦i − 𝜇)2
Al comparar los errores de estimació n (el numerador) con la variabilidad de los
valores originales (el denominador), R2 pretende capturar la proporció n de
variabilidad en un conjunto de datos que se explica por el modelo estadístico
proporcionado por el ajuste. Cuando el modelo que se evalú a se produce mediante
una regresió n lineal, el valor de R2 siempre se encuentra entre 0 y 1. Si R2 = 1, el
modelo explica toda la variabilidad de los datos. Si R2 = 0, no hay relació n entre los
valores predichos por el modelo y los datos reales.

El có digo de la figura 15.5 proporciona una implementació n sencilla de esta medida


estadística. Su compacidad se deriva de la expresividad de las operaciones en
matrices. La expresió n (predicho - medido)**2 resta los elementos de una matriz de
los elementos de otra y luego eleva al cuadrado cada elemento en el resultado. La
expresió n (medida - mediaDeMedida)**2 resta el valor escalar mediaDeMedida de
cada elemento de la matriz medida y luego eleva al cuadrado cada elemento de los
resultados.

Figura 15.5 Cálculo de R2


def rSquared(medido, predicho):
"""Asume medido una matriz unidimensional de valores medidos
predicho una matriz unidimensional de valores
97Hay varias definiciones diferentes del coeficiente de determinació n. La definició n
predichos
proporcionada aquí se utiliza
Devuelve para evaluarde
el coeficiente la calidad de un ajuste producido por una
determinación"""
regresió nerrorestimación
lineal. = ((predicho - medido)**2).sum()
mediaDeMedida = medida.sum()/float(len(medida))
variabilidad = ((medida - mediaDeMedida)**2) .sum()
Capítulo 15. Comprensión de los datos 2

Cuando las líneas de có digo


imprimir 'RSquare of linear fit =', rSquared(meanHeights, altitudes)

y
print 'RSquare of quadratic fit =', rSquared(meanHeights, altitudes)

se insertan despué s de las llamadas apropiadas


apylab.plotenprocesoTrayectorias, imprimen
RScuadrado de ajuste lineal =
0,0177433205441 RScuadrado de ajuste
cuadrático = 0,985765369287

En té rminos generales, esto nos dice que el modelo lineal puede explicar menos
del 2 % de la variació n en los datos medidos, pero el modelo cuadrá tico puede
explicar má s del 98 % de la variació n.

15.2.2 Usando un modelo computacional


Ahora que tenemos lo que parece ser un buen modelo de nuestros datos, podemos
usar este modelo para ayudar a responder preguntas sobre nuestros datos
originales. Una pregunta interesante es la velocidad horizontal a la que viaja el
proyectil cuando golpea el objetivo. Podríamos usar el siguiente tren de
pensamiento para diseñ ar un cá lculo que responda a esta pregunta:

1. Sabemos que la trayectoria del proyectil está dada por una fó rmula de la
formay = hacha2+ bx + c,es decir, es una pará bola. Como toda pará bola es
simé trica alrededor de su vé rtice, sabemos que su pico se encuentra a
medio camino entre el punto de lanzamiento y el objetivo; llama a este
valorxMid. La altura del pico,𝑦𝑃𝑒𝑎𝑘, por lo tanto viene dada por𝑦𝑃𝑒𝑎𝑘 = 𝑎 ∗
𝑥𝑀i𝑑2 + 𝑏 ∗ 𝑥𝑀i𝑑 + 𝑐.

2. Si ignoramos la resistencia del aire (recuerde que ningú n modelo es


perfecto), podemos calcular la cantidad de tiempo que tarda el proyectil en
caer desdeyPicoa la altura del objetivo, porque eso es puramente una funció n
de la gravedad. viene dada por la ecuacion 𝑡=(2 ∗ 𝑦𝑃𝑒𝑎𝑘)/𝑔.98Esta es
también la cantidad de tiempo que tarda el proyectil en recorrer la distancia
horizontal desdexMidal objetivo, porque una vez que alcanza el objetivo deja
de moverse.
3. Dado el tiempo para ir dexMidal objetivo, podemos calcular fá cilmente la
velocidad horizontal promedio del proyectil en ese intervalo. Si suponemos
que el proyectil no estaba acelerando ni desacelerando en la direcció n
horizontal durante ese intervalo, podemos usar la velocidad horizontal
promedio como una estimació n de la velocidad horizontal cuando el
proyectil golpea el objetivo.99
La figura 15.6 implementa esta técnica para estimar la velocidad horizontal del
proyectil.

98Estaecuació n se puede derivar de los primeros principios, pero es má s fá cil simplemente


buscarla. Lo encontramos
enhttp://en.wikipedia.org/wiki/Equations_for_a_falling_body.
99Lacomponente vertical de la velocidad tambié n se estima fá cilmente, ya que es
simplemente el producto de g y t en la figura 15.6.
2 Capítulo 15. Comprensión de los datos

def getHorizontalSpeed(a, b, c, minX, maxX):


"""Asume que minX y maxX son distancias en
pulgadas
Devuelve la velocidad horizontal en pies por
segundo""" pulgadas por pie = 12,0
xMid = (maxX - minX)/2.0
yPico = a*xMid**2 + b*xMid +
c
g = 32,16*pulgadas por pie #aceleración. de gravedad en

Figura 15.6 Cálculo de la velocidad horizontal de un proyectil

cuando la líneaobtenerVelocidadHorizontal(a, b, c, distancias[-1],


distancias[0])se inserta al final deprocesoTrayectorias, se imprime

Velocidad horizontal = 136 pies/seg

La secuencia de pasos que acabamos de trabajar sigue un patró n comú n.

1. Comenzamos realizando un experimento para obtener algunos datos


sobre el comportamiento de un sistema físico.
2. Luego usamos computació n para encontrar y evaluar la calidad de un
modelo del comportamiento del sistema.
3. Finalmente, usamos algo de teoría y aná lisis para diseñ ar un
cá lculo simple para derivar una consecuencia interesante del
modelo.

15.3 Ajuste de datos distribuidos exponencialmente


Polyfit utiliza la regresión lineal para encontrar un polinomio de
un grado determinado que sea el mejor ajuste de mínimos cuadrados
para algunos datos. Funciona bien si los datos se pueden aproximar
directamente mediante un polinomio. Pero esto no siempre es posible.
Considere, por ejemplo, la función de crecimiento exponencial
simpley = 2x. El código de la figura 15.7 se ajusta a un 4 elpolinomio de grado hasta los
primeros diez puntos y grafica los resultados. Utiliza la llamada de funció n
pylab.arange(10), que devuelve una matriz que contiene los nú meros enteros 0-9.

Figura 15.7 Ajuste de una curva polinomial a una distribución exponencial


valor = []
para i en el rango (10):
vals.append (2 ** i)
pylab.plot(vals,'bo', label = 'Puntos reales')
xVals = pylab.arange(10)
a,b,c,d,e = pylab.polyfit(xVals, vals, 4)
yVals = a*(xVals**4) + b*(xVals**3) + c*(xVals**2)+ d*xVals + e
pylab.plot(yVals, 'bx', label = 'Puntos predichos' , tamaño del
marcador = 20) pylab.title('Ajuste y = 2**x')
pylab.leyenda()
Capítulo 15. Comprensión de los datos 2

El có digo de la figura 15.7 produce el grá fico

El ajuste es claramente bueno, para estos puntos de datos. Sin embargo, veamos
lo que predice el modelo para 220. Cuando agregamos el có digo
pred2to20 = a*(20**4) + b*(20**3) + c*(20**2)+ d*20 + e
print 'El modelo predice que 2**20 es aproximadamente',
round(pred2to20) print 'El valor real de 2**20 es', 2**20

hasta el final de la figura 15.7, imprime,


El modelo predice que 2**20 es aproximadamente
29796.0 El valor real de 2**20 es 1048576

Dios mío, a pesar de ajustarse a los datos, el modelo producido por polyfit
aparentemente no es bueno. ¿Es porque cuatro no era el grado correcto? No. Es
porque ningú n polinomio se ajusta bien a una distribució n exponencial. ¿Significa
esto que no podemos usar polyfit para construir un modelo de una distribució n
exponencial?
Afortunadamente, no es así, porque podemos usar polyfit para encontrar una curva
que se ajuste a los valores independientes originales y al logaritmo de los valores
dependientes.

Considere la secuencia[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]. Si tomamos la base logarítmica2
de cada valor. obtenemos la secuencia [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], es decir, una
secuencia que crece linealmente. De hecho, si una funció n y = f(x), exhibe un
crecimiento exponencial, el logaritmo (en cualquier base) de f(x) crece linealmente.
Esto se puede visualizar trazando una funció n exponencial con un
eje y logarítmico. El có digo
xVals, yVals = [], []
para i en el rango
(10):
xVals.append(i)yVal
s.append(2**i)
pylab.plot(xVals, yVals)
pylab.semilogy()

produce la trama de la derecha.

El hecho de que tomar el logaritmo de


una funció n exponencial produce una
funció n lineal se puede usar para
construir una
2 Capítulo 15. Comprensión de los datos

modelo para un conjunto de puntos de datos distribuido exponencialmente, como


se ilustra en el có digo de la figura 15.8. Usamos polyfit para encontrar una curva
que se ajuste a los valores de x y al logaritmo de los valores de y. Tenga en cuenta
que usamos otro mó dulo de biblioteca está ndar de Python, matemá ticas, que
proporciona una funció n de registro.

importar matematicas

#define una función exponencial


arbitraria def f(x):
devuelve 3*(2**(1.2*x))

def crearExpData(f, xVals):


"""Supone que f es una función exponencial de un argumento
xVals es una matriz de argumentos adecuados para
f
Devuelve una matriz que contiene los resultados de
aplicar f a los elementos de xVals"""
yVals = []
for i in range(len(xVals)):
yVals.append(f(xVals[i]))
devuelve pylab.array(xVals), pylab.array(yVals)

def fitExpData(xVals, yVals):


"""Supone xVals y yVals matrices de números tales que
yVals[i] == f(xVals[i])
Devuelve a, b, base tal que log(f(x), base) == ax + b"""
logVals = []
para y en yVals:
logVals.append(math.log(y, 2.0)) #get log base 2
a,b = pylab.polyfit(xVals, logVals, 1)
devuelve a, b, 2.0

xVals, yVals = createExpData(f, range(10))


pylab.plot(xVals, yVals, 'ro', label = 'Actual value')
a, b, base = fitExpData(xVals, yVals)
predichoYVals = []
para x en xVals:
predichoYVals.append(base**(a*x + b))
pylab.plot(xVals, predictedYVals, label = 'Valores
predichos') pylab.title('Ajuste de una función exponencial')
pylab.legend()
#Mira un valor para x que no esté en los datos
originales imprime 'f(20) =', f(20)
imprimir 'f predicho (20) =', base ** (a * 20 + b)

Figura 15.8 Uso de polyfit para ajustar una distribución


exponencial

Cuando se ejecuta, este có digo


produce el grá fico de la derecha, en
el que coinciden los valores reales y
los valores pronosticados. Ademá s,
cuando el modelo se prueba con un
valor (20) que no se usó para
producir el ajuste, imprime
f(20) = 50331648.0
Predicho f(20) = 50331648.0
Capítulo 15. Comprensión de los datos 2

Este método de usar polyfit para encontrar un modelo para datos funciona cuando la
relació n se puede describir mediante una ecuació n de la forma y = baseax+b. Si se usa
con datos que no se pueden describir de esta manera, arrojará resultados erró neos.
Para ver esto, intentemos reemplazar el cuerpo de la funció n f por,
devuelve 3*(2**(1.2*x)) + x

Ahora imprime,
f(20) = 50331668.0
Predicho f (20) = 44846543.4909

15.4 Cuando falta la teoría


En este capítulo hemos enfatizado la interacció n entre la ciencia teó rica,
experimental y computacional. A veces, sin embargo, nos encontramos con muchos
datos interesantes, pero poca o ninguna teoría. En tales casos, a menudo recurrimos
al uso de té cnicas computacionales para desarrollar una teoría mediante la
construcció n de un modelo que parece ajustarse a los datos.

En un mundo ideal, haríamos un experimento controlado (p. ej., colgaríamos pesas


de un resorte), estudiaríamos los resultados y formularíamos retrospectivamente un
modelo consistente con esos resultados. Luego realizaríamos un experimento
prospectivo diferente (p. ej., colgaríamos diferentes pesos del mismo resorte) y
compararíamos los resultados de ese experimento con lo que predijo el modelo.

Desafortunadamente, en muchos casos es imposible ejecutar incluso un


experimento controlado. Imagine, por ejemplo, construir un modelo diseñ ado
para arrojar luz sobre có mo las tasas de interé s afectan los precios de las
acciones. Muy pocos de nosotros estamos en condiciones de establecer tasas de
interé s y ver qué sucede. Por otro lado, no faltan datos histó ricos relevantes.

En tales situaciones, se puede simular un conjunto de experimentos dividiendo los


datos existentes en un conjunto de entrenamiento y un conjunto reservado. Sin
mirar el conjunto reservado, construimos un modelo que parece explicar el
conjunto de entrenamiento. Por ejemplo, encontramos una curva que tiene un R2
razonable para el conjunto de entrenamiento. Luego probamos ese modelo en el
conjunto reservado. La mayoría de las veces, el modelo se ajustará má s al conjunto
de entrenamiento que al conjunto reservado. Pero si el modelo es bueno, debería
encajar razonablemente bien en el conjunto reservado. Si no es así, el modelo
probablemente debería descartarse.

¿Có mo se elige el conjunto de entrenamiento? Queremos que sea representativo


del conjunto de datos como un todo. Una forma de hacerlo es elegir aleatoriamente
las muestras para el conjunto de entrenamiento. Si el conjunto de datos es lo
suficientemente grande, a menudo funciona bastante bien.

Una forma relacionada pero ligeramente diferente de verificar un modelo es


entrenar en muchos subconjuntos seleccionados al azar de los datos originales y
ver qué tan similares son los modelos entre sí. Si son bastante similares,
podemos sentirnos bastante bien. Este enfoque se conoce como validació n
cruzada.
16 MENTIRAS, MALDITAS MENTIRAS, Y
ESTADÍSTICAS

“Si no puede probar lo que quiere probar, demuestre otra cosa y finja que
son lo mismo. En el aturdimiento que sigue a la colisión de las estadísticas
con la mente humana, casi nadie notará la diferencia”. 100

El pensamiento estadístico es un invento relativamente nuevo. Durante la mayor


parte de la historia registrada, las cosas se evaluaron cualitativamente má s que
cuantitativamente. Las personas deben haber tenido un sentido intuitivo de
algunos hechos estadísticos (por ejemplo, que las mujeres suelen ser má s bajas
que los hombres), pero no tenían herramientas matemá ticas que les permitieran
pasar de la evidencia anecdó tica a conclusiones estadísticas. Esto comenzó a
cambiar a mediados del siglo XVII, sobre todo con la publicació n de Natural and
Political Observations Made Upon the Bills of Mortality de John Graunt. Este
trabajo pionero utilizó el aná lisis estadístico para estimar la població n de
Londres a partir de las listas de muertos e intentó proporcionar un modelo que
podría usarse para predecir la propagació n de la peste.

Desde entonces, la gente ha utilizado las estadísticas tanto para engañ ar como para
informar. Algunos han utilizado deliberadamente las estadísticas para engañ ar;
otros simplemente han sido incompetentes. En este capítulo analizamos algunas
formas en las que se puede engañ ar a la gente para que haga inferencias
inapropiadas a partir de datos estadísticos. Confiamos en que usará esta
informació n solo para el bien, es decir, para convertirse en un mejor consumidor y
un proveedor má s honesto de informació n estadística.

16.1 Basura entra basura sale (GIGO)


“En dos ocasiones [miembros del Parlamento] me han preguntado: 'Por
favor, señor Babbage, si ingresa cifras incorrectas en la máquina, ¿saldrán
las respuestas correctas?' No soy capaz de aprehender correctamente el
tipo de confusión de ideas que podría provocar tal pregunta.”- Charles
Babbage.

El mensaje aquí es simple. Si los datos de entrada tienen fallas graves, ninguna
cantidad de masaje estadístico producirá un resultado significativo.

El censo de Estados Unidos de 1840 mostró que la locura entre los negros y
mulatos libres era aproximadamente diez veces má s comú n que entre los
negros y mulatos esclavizados. La conclusió n era obvia. Como dijo el senador
estadounidense (y exvicepresidente y futuro secretario de Estado) John C.
Calhoun: “Los datos sobre locura revelados en este censo son intachables. De
ella nuestra nació n debe concluir que la abolició n de la esclavitud sería una
maldició n para los africanos”. No importa que pronto quedó claro que el
censo estaba plagado de errores. Como supuestamente explicó Calhoun a John
Quincy Adams, “hubo tantos errores que

100Darrell Huff, Có mo mentir con estadísticas, 1954.


Capítulo 16. Mentiras, malditas mentiras y 2

se equilibraron entre sí y llevaron a la misma conclusió n como si todos fueran


correctos”.

La respuesta espuria (quizá s deliberadamente) de Calhoun a Adams se basó en


un error clá sico, la suposició n de la independencia. Si hubiera sido má s
sofisticado matemá ticamente, podría haber dicho algo como: "Creo que los
errores de medició n son imparciales e independientes entre sí y, por lo tanto, se
distribuyen uniformemente a ambos lados de la media". De hecho, aná lisis
posteriores mostraron que los errores estaban tan fuertemente sesgados que no
se podían extraer conclusiones estadísticamente vá lidas.101

16.2 Las imágenes pueden ser engañosas


No puede haber dudas sobre la utilidad de los grá ficos para transmitir
informació n rá pidamente. Sin embargo, cuando se usa sin cuidado (o
maliciosamente), una trama puede ser muy engañ osa. Considere, por ejemplo,
los siguientes grá ficos que muestran los precios de la vivienda en los estados del

medio oeste de EE. UU.


Mirando el grá fico de la izquierda, parece que los precios de la vivienda se
mantuvieron bastante estables entre 2006 y 2009. Pero espere un minuto, ¿no
hubo un colapso de los bienes raíces residenciales en EE. UU. seguido de una
crisis financiera mundial a fines de 2008? De hecho, la hubo, como se muestra
en el grá fico de la derecha.

Estos dos grá ficos muestran exactamente los mismos datos, pero transmiten
impresiones muy diferentes.

El primer grá fico fue diseñ ado para dar la impresió n de que los precios de la
vivienda se habían mantenido estables. En el eje Y, el diseñ ador usó una escala
logarítmica que va desde el absurdamente bajo precio promedio de una casa de
$10,000 hasta el increíblemente alto precio promedio de $1 milló n. Esto minimizó la
cantidad de espacio dedicado al á rea donde los precios está n cambiando, dando la
impresió n de que los cambios fueron relativamente pequeñ os. El grá fico de arriba y
de la derecha fue diseñ ado para dar la impresió n de que los precios de la vivienda se
movieron de manera errá tica y luego se desplomaron. El

101Debemos tener en cuenta que Calhoun estuvo en el cargo hace má s de 150 añ os. No hace
falta decir que ningú n político contemporá neo encontraría formas de abusar de las estadísticas
para respaldar una posició n.
2 Capítulo 16. Mentiras, malditas mentiras y

El diseñ ador usó una escala lineal y un rango estrecho de precios, por lo que se
exageraron los tamañ os de los cambios.

El có digo de la figura 16.1 produce los dos grá ficos que vimos anteriormente y un
grá fico destinado a dar una impresió n precisa del movimiento de los precios de la
vivienda.

Utiliza dos instalaciones de trazado que aú n no hemos visto. La


llamadapylab.bar(cuartos, precios, ancho)produce un grá fico de barras
conanchobarras anchas. Los bordes izquierdos de las barras son los valores de los
elementos decuartelesy las alturas de las barras son los valores de los elementos
correspondientes deprecios. La llamadapylab.xticks(cuartos+ancho/2.0,
etiquetas)describe las etiquetas asociadas con las barras. El primer argumento especifica
dó nde se colocará cada etiqueta y el segundo argumento el texto de las etiquetas. La
funció ngarrapatasse comporta de manera aná loga.

Figura 16.1 Trazado de los precios de la vivienda


def plotVivienda(impresión):
"""Asume la impresión de una cadena. Debe ser 'plana',
'volátil' y 'justa'
Produce un gráfico de barras de los precios de
la vivienda a lo largo del tiempo""" f =
open('midWestHousingPrices.txt', 'r')
#Cada línea del archivo contiene el precio del
trimestre del año #para la región del medio
oeste de EE. UU.
etiquetas, precios =
([], []) para línea en
f:
año, trimestre, precio = línea.split()
etiqueta = año[2:4] + '\n Q' +
trimestre[1]
etiquetas.append(etiqueta)
precios.append(float(precio)/1000)
quarters = pylab.arange(len(labels)) #x coords of bars
width = 0.8 #Width of bars
if impresión ==
'plano':
pylab.semilogy()
pylab.bar(trimestres, precios, ancho)
pylab.xticks(trimestres+ancho/2.0, etiquetas)
pylab.title('Precios de la vivienda en el medio oeste de
EE. UU.') pylab.xlabel('Trimestre')
pylab.ylabel('Precio promedio
($1,000\'s)') si la impresión == 'plana':
pylab.ylim(10, 10**3)
impresión elif == 'volátil':
pylab.ylim(180, 220)
elif impresión ==
'justo':
pylab.ylim(150,
250) más:
aumentar ValueError
Capítulo 16. Mentiras, malditas mentiras y 2

La llamadaplotVivienda('justa')produce la trama

16.3 Cum Hoc Ergo Propter Hoc102


Se ha demostrado que los estudiantes universitarios que asisten regularmente a
clase tienen calificaciones promedio má s altas que los estudiantes que asisten a
clase solo esporá dicamente. A los que impartimos estas clases nos gustaría creer
que esto se debe a que los estudiantes aprenden algo de las conferencias. Por
supuesto, es al menos igualmente probable que esos estudiantes obtengan
mejores calificaciones porque los estudiantes que tienen má s probabilidades de
asistir a clases tambié n tienen má s probabilidades de estudiar mucho.

Cuando dos cosas está n correlacionadas,103 existe la tentació n de asumir que una
ha causado la otra. Considere la incidencia de la gripe en América del Norte. El
nú mero de casos sube y baja en un patró n predecible. Casi no hay casos en el
verano, la cantidad de casos comienza a aumentar a principios del otoñ o y luego
comienza a disminuir a medida que se acerca el verano. Ahora considere el nú mero
de niñ os que asisten a la escuela. Hay muy pocos niñ os en la escuela en el verano, la
inscripció n comienza a aumentar a principios del otoñ o y luego cae a medida que se
acerca el verano.

La correlació n entre la apertura de escuelas y el aumento de la incidencia de la


gripe es indiscutible. Esto ha llevado a muchos a concluir que ir a la escuela es un
factor causal importante en la propagació n de la gripe. Eso podría ser cierto, pero
uno no puede concluirlo basá ndose simplemente en la correlació n. ¡La correlació n
no implica causa! Despué s de todo, la correlació n podría usarse con la misma
facilidad para justificar la creencia de que los brotes de gripe hacen que las
escuelas esté n en sesió n. O tal vez no hay una relació n causal en ninguna direcció n,
y hay alguna variable al acecho que

102Los estadísticos, como los abogados y los médicos, a veces usan el latín sin otra razó n
obvia que la de parecer eruditos. Esta frase significa, “con esto, por lo tanto a causa de
esto”.
103La correlació n es una medida del grado en que dos variables se mueven en la misma
direcció n. Si cuando x sube y sube, las variables está n correlacionadas positivamente. Si
se mueven en direcciones opuestas, se correlacionan negativamente. Si no hay relació n, la
correlació n es 0. Las alturas de las personas se correlacionan positivamente con las
alturas de sus padres. La correlació n entre las horas dedicadas a los videojuegos y el
promedio de calificaciones es negativa.
2 Capítulo 16. Mentiras, malditas mentiras y

no hemos considerado que causas cada uno. De hecho, da la casualidad de que el


virus de la gripe sobrevive mucho má s tiempo en el aire fresco y seco que en el aire
cá lido y hú medo, y en Amé rica del Norte tanto la temporada de gripe como las
sesiones escolares se correlacionan con un clima má s frío y seco.

Con suficientes datos


retrospectivos, siempre es
posible encontrar dos
variables que esté n
correlacionadas, como se
ilustra en el grá fico de la
derecha.104 Cuando se
encuentran tales
correlaciones, lo primero que
se debe hacer es preguntar si
existe una
teoría plausible que explica la
correlació n.

Caer presa de la falacia cum


hoc ergo propter hoc puede ser
bastante peligroso. A principios de 2002, aproximadamente a seis millones de
mujeres estadounidenses se les recetaba terapia de reemplazo hormonal (TRH)
con la creencia de que reduciría sustancialmente el riesgo de enfermedades
cardiovasculares. Esa creencia fue respaldada por varios estudios publicados de
gran reputació n que demostraron una incidencia reducida de muerte
cardiovascular entre las mujeres que usan TRH. Muchas mujeres y sus mé dicos
se sorprendieron cuando el Journal of the American Medical Society publicó un
artículo que afirmaba que la TRH, de hecho, aumentaba el riesgo de enfermedad
cardiovascular.105 ¿Có mo pudo haber sucedido esto?

El nuevo aná lisis de algunos de los estudios anteriores mostró que las mujeres
que se sometían a TRH probablemente pertenecían a grupos con regímenes de
dieta y ejercicio mejores que el promedio. Tal vez las mujeres que se sometieron
a la TRH eran, en promedio, má s conscientes de su salud que las otras mujeres
del estudio, por lo que tomar la TRH y mejorar la salud cardíaca fueron efectos
coincidentes de una causa comú n.

16.4 Las medidas estadísticas no cuentan toda la historia


Hay una enorme cantidad de estadísticas diferentes que se pueden extraer de un
conjunto de datos. Al elegir cuidadosamente entre estos, es posible transmitir una
variedad de impresiones diferentes sobre los mismos datos. Un buen antídoto es
observar el propio conjunto de datos.

En 1973, el estadístico FJ Anscombe publicó un artículo que contenía la


siguiente tabla. Contiene las coordenadas <x, y> de los puntos en cada uno de los
cuatro conjuntos de datos.

104Stephen R. Johnson, "El problema con QSAR (o có mo aprendí a dejar de preocuparme y aceptar
la falacia)", J. Chem. informació n Modelo., 2008.
105Nelson HD, Humphrey LL, Nygren P, Teutsch SM, Allan JD. Terapia de reemplazo hormonal
Capítulo 16. Mentiras, malditas mentiras y
posmenopá usica: revisió n científica. JAMA. 2002;288:872-881.
2
2 Capítulo 16. Mentiras, malditas mentiras y

X y X y X y X y
10.0 8.04 10.0 9.14 10.0 7.46 8.0 6.58
8.0 6.95 8.0 8.14 8.0 6.77 8.0 5.76
13.0 7.58 13.0 8.74 13.0 12.74 8.0 7.71
9.0 8.81 9.0 8.77 9.0 7.11 8.0 8.84
11.0 8.33 11.0 9.26 11.0 7.81 8.0 8.47
14.0 9.96 14.0 8.10 14.0 8.84 8.0 7.04
6.0 7.24 6.0 6.13 6.0 6.08 8.0 5.25
4.0 4.26 4.0 3.10 4.0 5.39 19.0 12.50
12.0 10.84 12.0 9.13 12.0 8.15 8.0 5.56
7.0 4.82 7.0 7.26 7.0 6.42 8.0 7.91
5.0 5.68 5.0 4.74 5.0 5.73 8.0 6.89

Estos cuatro conjuntos de datos son estadísticamente similares. Tienen el mismo


valor medio de x (9,0), el mismo valor medio de y (7,5), la misma varianza de x
(10,0), la misma varianza de y (3,75) y la misma correlació n entre x e y (0,816). ).
Ademá s, si usamos la regresió n lineal para ajustar una línea a cada uno,
obtenemos el mismo resultado para cada uno, y = 0.5x + 3.

¿Significa esto que no hay una manera obvia de distinguir estos conjuntos de
datos entre sí? No, uno simplemente necesita graficar los datos para ver que los
conjuntos de datos no se parecen en nada.

La moraleja es simple: si es posible, siempre eche un vistazo a alguna representació n


de los datos sin procesar.
Capítulo 16. Mentiras, malditas mentiras y 2

16.5 Sesgo de muestreo


Durante la Segunda Guerra Mundial, cada vez que un avió n aliado regresaba de una
misió n sobre Europa, el avió n era inspeccionado para ver dó nde había impactado el
fuego antiaéreo. Con base en estos datos, los mecá nicos reforzaron aquellas á reas de
los aviones que parecían má s propensas a ser atacadas por fuego antiaé reo.

¿Qué tiene de malo esto? No inspeccionaron los aviones que no regresaron de las
misiones porque habían sido derribados por fuego antiaé reo. Quizá s estos
aviones no examinados no pudieron regresar precisamente porque fueron
alcanzados en los lugares donde el fuego antiaé reo causaría el mayor dañ o. Este
error particular se llama sesgo de no respuesta. Es bastante comú n en las
encuestas. En muchas universidades, por ejemplo, se les pide a los estudiantes
durante una de las conferencias al final del trimestre que completen un
formulario que califica la calidad de las conferencias del profesor. Aunque los
resultados de tales encuestas a menudo son poco halagadores, podrían ser
peores. Aquellos estudiantes que piensan que las conferencias son tan malas que
no vale la pena asistirlas no está n incluidos en la encuesta.106

Como dijimos antes, todas las té cnicas estadísticas se basan en la suposició n de


que al muestrear un subconjunto de una població n podemos inferir cosas sobre
la població n en su conjunto. Si se usa el muestreo aleatorio, podemos hacer
declaraciones matemá ticas precisas sobre la relació n esperada de la muestra con
la població n total. Desafortunadamente, muchos estudios, particularmente en las
ciencias sociales, se basan en lo que se ha llamado muestreo por conveniencia (o
accidental). Esto implica elegir muestras en funció n de lo fá ciles que sean de
adquirir. ¿Por qué tantos estudios psicoló gicos utilizan poblaciones de
estudiantes universitarios? Porque son fá ciles de encontrar en los campus
universitarios. Una muestra de conveniencia puede ser representativa, pero no
hay forma de saber si realmente lo es.

El sitio web del Family Research Institute contiene una tabla con la siguiente
informació n:

Cuadro 1: ¿Cuánto tiempo viven los homosexuales? 107

106Elcambio a las encuestas en línea, que permite que los estudiantes que no asisten a clase
participen en la encuesta, no es un buen augurio para los egos de los profesores.
107http://www.familyresearchinst.org/2012/01/how-long-do-homosexuals-live/
2 Capítulo 16. Mentiras, malditas mentiras y

Cosas bastante aterradoras si tu preferencia sexual no es heterosexual, hasta que


uno mira có mo se compilaron los datos. Segú n el sitio web, se basó en "6.737
obituarios de 18 revistas homosexuales de EE. UU., en comparació n con los
obituarios de 2 perió dicos principales".

Este mé todo produce una muestra que podría no ser representativa de la població n
homosexual o no homosexual (o de ambas) por un gran nú mero de razones. Por
ejemplo, parece inferir que alguien es gay o lesbiana si y solo si su obituario aparece
en un “diario homosexual”, y que alguien no es gay si su obituario aparece en un
“perió dico convencional”. Tambié n parece suponer que las muertes para las que
aparecen los obituarios son representativas de todas las muertes. ¿Có mo se hace
para evaluar una muestra así? Una té cnica es comparar los datos compilados de la
muestra con los datos compilados en otros lugares. Por ejemplo, uno podría
comparar la proporció n de hombres homosexuales a hombres heterosexuales en el
estudio de obituario con otros estudios que informan los tamañ os relativos de esas
dos poblaciones.

16.6 El contexto importa


Es fá cil leer má s en los datos de lo que realmente implican, especialmente cuando
se ven los datos fuera de contexto. El 29 de abril de 2009, CNN informó que “los
funcionarios de salud mexicanos sospechan que el brote de gripe porcina ha
causado má s de 159 muertes y aproximadamente 2500 enfermedades”. Algo
bastante aterrador, hasta que uno lo compara con las 36 000 muertes atribuibles
anualmente a la gripe estacional en los EE. UU.

Una estadística a menudo citada y precisa es que la mayoría de los accidentes


automovilísticos ocurren dentro de las 10 millas de la casa. Entonces, ¿qué? La
mayor parte de la conducció n se realiza dentro de las 10 millas de la casa. Y ademá s,
¿qué significa “hogar” en este contexto? La estadística se calcula utilizando la
direcció n en la que el automó vil está registrado como "domicilio". ¿Se podría
reducir la probabilidad de tener un accidente simplemente registrando el auto en
algú n lugar distante?

A los que se oponen a las iniciativas del gobierno para reducir la prevalencia de
las armas en los EE. UU. les gusta citar la estadística de que aproximadamente el
99,8 % de las armas de fuego en los EE. UU. no se utilizará n para cometer un
delito violento en un añ o determinado. ¿Significa esto que no hay mucha
violencia armada en los Estados Unidos? La Asociació n Nacional del Rifle informa
que hay aproximadamente 300 millones de armas de fuego de propiedad privada
en los EE. UU.: el 0,2% de 300 millones son 600.000.

16.7 Cuidado con la extrapolación


Es demasiado fá cil extrapolar a partir de los datos. Hicimos eso en el Capítulo 15
cuando ampliamos los ajustes derivados de la regresió n lineal má s allá de los
datos sobre los que se realizó la regresió n. La extrapolació n debe hacerse solo
cuando se tiene una justificació n teó rica só lida para hacerlo. Se debe tener
especial cuidado con las extrapolaciones de línea recta.
Capítulo 16. Mentiras, malditas mentiras y 2

Considere la trama de la izquierda.


Muestra el crecimiento del uso de
Internet en los Estados Unidos desde
1994 hasta 2000. Como puede ver,
una línea recta proporciona un
ajuste bastante bueno.

La grá fica de la derecha usa este ajuste


para proyectar el porcentaje de la
població n estadounidense que usa Internet
en los añ os siguientes. La proyecció n es un
poco difícil de creer. Parece poco probable
que para 2009 todos en el
Estados Unidos estaba usando Internet, y menos aú n
Es probable que para 2012 má s del 120 % de la població n de EE. UU. estuviera
usando Internet.

16.8 La falacia del francotirador de Texas


Imagine que está conduciendo por una carretera rural en Texas. Ves un granero
que tiene seis dianas pintadas y un agujero de bala en el centro de cada diana. “Sí
señ or”, dice el dueñ o del granero, “nunca fallo”. "Así es", dice su có nyuge, "no hay
un hombre en el estado de Texas que sea má s preciso con un pincel". ¿Entiendo?
Disparó los seis tiros y luego pintó los blancos a su alrededor.
Profesor desconcertado sobre la habilidad de tirar tiza de los

estudiantes

Un clá sico del gé nero apareció en 2001.108 Informó que un equipo de


investigació n del hospital Royal Cornhill en Aberdeen había descubierto que “las
mujeres anoré xicas tienen má s probabilidades de haber nacido en la primavera o
principios del verano... Entre

108Eagles,John, et al., "Season of birth in females with anorexia nervosa in Northeast


Scotland", International Journal of Eating Disorders, 30, 2, septiembre de 2001.
2 Capítulo 16. Mentiras, malditas mentiras y

marzo y junio nacieron un 13% má s de anoré xicas que la media, y un 30%


má s en junio mismo.”

Miremos esa estadística preocupante para aquellas mujeres nacidas en junio. El


equipo estudió a 446 mujeres a las que se les había diagnosticado anorexia, por
lo que la media de nacimientos por mes fue ligeramente superior a 37. Esto
sugiere que el nú mero de nacidos en junio fue de 48 (37*1,3). Escribamos un
programa corto para ver si podemos rechazar la hipó tesis nula de que esto
ocurrió puramente por casualidad.

def Figura 16.2 Probabilidad de que nazcan 48 anoréxicas en junio


junioProb(númPrueb
cuando corrimos
as): junio48 = 0
junioProb(10000) imprimió
para juicio en rango
Probabilidad de al
(numTrials): menos
junio 48 nacimientos en junio = 0,044
= 0
para i en el rango (446):
Parece que la probabilidad de que al menos 48 bebé s nazcan en junio por pura
si aleatorio.randint(1,12)
casualidad es de==alrededor
6: juniodel+=4,5
1 %. Entonces, tal vez esos investigadores en
Aberdeen
si esté n en
junio >=lo cierto. Bueno, podrían haber dado en algo si hubieran
comenzado 48:
con la hipó tesis de que nacerá n má s bebé s que se volverá n
junio48
anoré xicos en
+= junio,
1 y luego realizaron un estudio diseñ ado para verificar esa
hipó tesis.

Pero eso no es lo que hicieron. En cambio, miraron los datos y luego, imitando
al francotirador de Texas, dibujaron un círculo alrededor de junio. La pregunta
estadística correcta que se debe haber hecho es cuá l es la probabilidad de que
haya habido al menos un mes (de 12) en el que nacieran al menos 48 bebé s. El
programa de la figura 16.3 responde a esa pregunta.
Figura 16.3 Probabilidad de que nazcan 48 anoréxicas en algún mes
def
anyProb(numTrials
): anyMonth48 = 0
para prueba en rango
(numTrials): meses = [0] *
12
for i in range(446):
meses[random.randint(0,11)] +=
1
si max(meses) >=
48: anyMonth48
Capítulo 16. Mentiras, malditas mentiras y 2

La llamadacualquierProb(10000)impreso
Probabilidad de al menos 48 nacimientos en algún mes = 0,446

Parece que, despué s de todo, no es tan improbable que los resultados informados en
el estudio reflejen una ocurrencia fortuita en lugar de una asociació n real entre el
mes de nacimiento y la anorexia. Uno no tiene que venir de Texas para ser víctima de
la falacia del francotirador de Texas.

Lo que vemos aquí es que la importancia estadística de un resultado depende de


la forma en que se realizó el experimento. Si el grupo de Aberdeen hubiera
partido de la hipó tesis de que en junio nacen má s anoré xicos, valdría la pena
considerar su resultado. Pero si partieron de la hipó tesis de que existe un mes en
el que nace una proporció n inusualmente grande de anoré xicos, su resultado no
es muy convincente.

¿Qué pró ximos pasos podría haber tomado el grupo de Aberdeen para probar su
nueva hipó tesis? Una posibilidad es realizar un estudio prospectivo. En un estudio
prospectivo, uno comienza con un conjunto de hipó tesis y luego recopila datos con
el potencial de refutar o confirmar la hipó tesis. Si el grupo realizara un nuevo
estudio y obtuviera resultados similares, uno podría estar convencido.

Los estudios prospectivos pueden ser costosos y lentos de realizar. En un estudio


retrospectivo, uno tiene que examinar los datos existentes de manera que se
reduzca la probabilidad de obtener resultados engañ osos. Una técnica comú n, como
se discutió en el Capítulo 15, es dividir los datos en un conjunto de entrenamiento y
un conjunto reservado. Por ejemplo, podrían haber elegido 446/2 mujeres al azar
de sus datos (el conjunto de entrenamiento) y contar el nú mero de nacimientos de
cada mes. Luego podrían haber comparado eso con el nú mero de nacimientos cada
mes para las mujeres restantes (el conjunto reservado).

16.9 Los porcentajes pueden confundir


Un asesor de inversiones llamó a un cliente para informarle que el valor de su
cartera de acciones había subido un 16 % durante el ú ltimo mes. Admitió que hubo
algunos altibajos durante el añ o, pero se complació en informar que el cambio
mensual promedio fue de +0.5%. Imagínese la sorpresa del cliente cuando recibió
su estado de cuenta del añ o y observó que el valor de su cartera había disminuido
durante el añ o.

Llamó a su asesor y lo acusó de mentiroso. “Me parece”, dijo, “que mi cartera


disminuyó un 0,67 % y usted me dijo que aumentó un 0,5 % al mes”. “No lo
hice”, respondió el asesor financiero, “te dije que el cambio promedio mensual
fue de +0.5%”. Cuando examinó sus estados de cuenta mensuales, el
inversionista se dio cuenta de que no le habían mentido, solo lo habían
engañ ado. Su cartera bajó un 15% cada mes durante la primera mitad del añ o y
luego subió un 16% cada mes durante la segunda mitad del añ o.

Cuando pensamos en porcentajes, siempre debemos prestar atenció n a la base


sobre la cual se calcula el porcentaje. En este caso, las disminuciones del 15%
fueron en promedio má s altas que los aumentos del 16%.
2 Capítulo 16. Mentiras, malditas mentiras y

Los porcentajes pueden ser particularmente engañ osos cuando se aplican a una
base pequeñ a. Es posible que lea acerca de un medicamento que tiene el efecto
secundario de aumentar la incidencia de alguna enfermedad en un 200 %. Pero si la
incidencia base de la enfermedad es muy baja, digamos uno en 1,000,000, bien
podría decidir que el riesgo de tomar el medicamento fue má s que contrarrestado
por los efectos positivos del medicamento.

16.10 solo ten cuidado


Sería fá cil y divertido llenar unos pocos cientos de pá ginas con una historia de
abusos estadísticos. Pero a estas alturas probablemente ya entendiste el mensaje: es
tan fá cil mentir con nú meros como mentir con palabras. Asegú rese de comprender
qué se está midiendo realmente y có mo se calcularon esos resultados
"estadísticamente significativos" antes de sacar conclusiones precipitadas.
Capítulo 16. Mentiras, malditas mentiras y 2

17 PROBLEMAS DE OPTIMIZACIÓN DE GRÁFICOS Y


MOCHILA

La noció n de un problema de optimizació n proporciona una forma estructurada de


pensar en la resolució n de muchos problemas computacionales. Cada vez que se
propone resolver un problema que implica encontrar el mayor, el menor, el
má ximo, el menor nú mero, el má s rá pido, el menos costoso, etc., existe una buena
posibilidad de que pueda mapear el problema en un problema de optimizació n
clá sico para el cual existe una solució n computacional conocida.

En general, un problema de optimizació n tiene dos partes:

1. Una funció n objetivo que se va a maximizar o minimizar. Por


ejemplo, el billete de avió n entre Boston y Estambul.
2. Un conjunto de restricciones (posiblemente vacío) que se debe
respetar. Por ejemplo, un límite superior en el tiempo de viaje.

En este capítulo, presentamos la noció n de un problema de optimizació n y damos


algunos ejemplos. También proporcionamos algunos algoritmos simples que los
resuelven. En el siguiente capítulo, analizamos formas má s eficientes de resolver
una clase importante de problemas de optimizació n.

Los principales puntos a destacar de este capítulo son:

 Muchos problemas de verdadera importancia pueden formularse de


forma sencilla de forma que conduzcan de forma natural a una solució n
computacional.
 Reducir un problema aparentemente nuevo a una instancia de un
problema bien conocido permite utilizar soluciones preexistentes.
 Los algoritmos de enumeració n exhaustiva proporcionan una forma
simple, pero a menudo intratable desde el punto de vista
computacional, de buscar soluciones ó ptimas.
 Un algoritmo codicioso es a menudo un enfoque prá ctico para encontrar
una solució n bastante buena, pero no siempre ó ptima, a un problema de
optimizació n.
 Los problemas de mochila y los problemas de grá ficos son clases de
problemas a los que a menudo se pueden reducir otros problemas.

Como de costumbre, complementaremos el material sobre pensamiento


computacional con algunos fragmentos de Python y algunos consejos sobre
programació n.

17.1 Problemas con la mochila


No es fá cil ser un ladró n. Ademá s de los problemas obvios (asegurarse de que una
casa esté vacía, abrir cerraduras, eludir alarmas, lidiar con dilemas é ticos, etc.), un
ladró n tiene que decidir qué robar. El problema es que la mayoría de las casas
contienen má s cosas de valor de las que el ladró n promedio puede llevarse. ¿Qué
puede hacer un pobre ladró n? Necesita encontrar el conjunto de cosas que
proporcione el mayor valor sin exceder su capacidad de carga.
Capítulo 17. Problemas de optimización de gráfico y 2

Supongamos, por ejemplo, que un ladró n que tiene una mochila109 que puede
contener como má ximo 20 libras de botín irrumpe en una casa y encuentra los
artículos de la figura 17.1. Claramente, no podrá meterlo todo en su mochila, por
lo que debe decidir qué llevar y qué dejar atrá s.

Valor Peso Valor de peso


Reloj 175 10 17.5
Cuadro 90 9 10
Radio 20 4 5
Florero 50 2 25
Libro 10 1 10
Computad 200 20 10
ora

Figura 17.1 Tabla de elementos

17.1.1 Algoritmos codiciosos


La forma má s sencilla de encontrar una solució n aproximada a este problema es
utilizar un algoritmo voraz. El ladró n elegiría primero el mejor artículo, luego el
siguiente y continuaría hasta llegar a su límite. Por supuesto, antes de hacer esto,
el ladró n tendría que decidir qué debería significar "mejor". ¿El mejor artículo es
el má s valioso, el menos pesado o quizá s el artículo con la relació n valor-peso má s
alta? Si elegía el valor má s alto, se iría solo con la computadora, que podría vender
por $200. Si eligiera el peso má s bajo, tomaría, en orden, el libro, la radio, el jarró n
y la pintura, que valdrían un total de
$170. Finalmente, si decidiera que lo mejor significaba la mayor relación valor-peso, comenzaría
por tomar el jarrón y el reloj. Eso dejaría tres artículos con una relación valor-peso de 10, pero de
esos solo el libro cabría en la mochila. Después de tomar el libro, tomaría el artículo restante que
aún le quedaba, la radio. El valor total de su botín sería de $255.

Aunque el codicioso por densidad (relació n valor-peso) produce el mejor resultado


para este conjunto de datos, no hay garantía de que un algoritmo codicioso por
densidad siempre encuentre una mejor solució n que el codicioso por peso o valor.
En té rminos má s generales, no hay garantía de que cualquier solució n a este tipo de
problema de la mochila que se encuentre mediante un algoritmo codicioso sea
ó ptima.110 Discutiremos este problema con má s detalle un poco má s adelante.

El có digo de la figura 17.2 y la figura 17.3 implementa estos tres algoritmos


codiciosos. En la Figura 17.2, primero definimos la clase Elemento. Cada
elemento tiene un atributo de nombre, valor y peso.

109Para aquellos de ustedes que son demasiado jó venes para recordar, una "mochila" es
una bolsa simple que la gente solía llevar en la espalda, mucho antes de que las "mochilas"
se pusieran de moda. Si por casualidad has estado en la exploració n, quizá s recuerdes las
palabras del “Caminante feliz”, “Me encanta andar deambulando, A lo largo del sendero de
la montañ a, Y mientras avanzo, Me encanta cantar, Con la mochila a la espalda. ”
110Probablementehaya alguna lecció n moral profunda que se pueda extraer de este hecho, y
probablemente no sea "la codicia es buena".
2 Capítulo 17. Problemas de optimización de gráfico y

El ú nico có digo interesante es la implementació n de la funció n greedy. Al


introducir el pará metro keyFunction, hacemos que greedy sea independiente del
orden en que se deben considerar los elementos de la lista. Todo lo que se requiere
es que keyFunction defina un orden en los elementos de los artículos. Luego
usamos este orden para producir una lista ordenada que contiene los mismos
elementos que los artículos.
Usamos la funció n integrada de Python ordenada para hacer esto. (Usamos sorted en
lugar de sort porque queremos generar una nueva lista en lugar de mutar la lista
pasada a la funció n). Usamos el pará metro inverso para indicar que queremos que la
lista se ordene de mayor (con respecto a keyFunction) a menor.

Figura 17.2 Creación de un conjunto de elementos con pedidos


elemento de clase (objeto):
def __init__(self, n, v,
w): self.nombre = n
self.value =
float(v) self.weight
= float(w)
def getName(self):
return
self.nombre
def getValue(self):
devuelve
self.value
def getWeight(self):
return self.peso
def __str__(uno mismo):
resultado = '<' + self.name + ', ' + str(self.value)\
+ ', ' + str(self.weight) +
'>' devuelve el resultado

valor def (elemento):


devolver artículo.getValue()

def pesoInverso(elemento):
return
1.0/elemento.obtenerPeso(
)

densidad de definición (elemento):


devolver artículo.obtenerValor()/artículo.obtenerPeso()

def buildItems():
nombres = ['reloj', 'pintura', 'radio', 'jarrón', 'libro',
'computadora'] valores = [175,90,20,50,10,200]
Capítulo 17. Problemas de optimización de gráfico y 2

def greedy(items, maxWeight, keyFunction):


"""Asume Items a list, maxWeight >= 0,
keyFunction asigna elementos de Items a floats"""
itemsCopy = sorted(items, key=keyFunction, reverse = True)
result = []
valor total = 0.0
peso total = 0.0
for i in range(len(itemsCopy)):
if (totalWeight + itemsCopy[i].getWeight()) <= maxWeight:
result.append(itemsCopy[i])
totalWeight += itemsCopy[i].getWeight()
totalValue += itemsCopy[i].getValue()
retorno (resultado, valor total)

def testGreedy (elementos, restricción, función clave):


tomado, val = codicioso(artículos, restricción,
función clave) print 'Valor total de los artículos
tomados = ', val
para el artículo
tomado:
imprimir'',

artículo

def testGreedys(maxWeight =
20): items = buildItems()
imprimir 'Usar greedy por valor para llenar mochila de tamaño',
maxWeight testGreedy(items, maxWeight, value)

Figura 17.3 Uso de un algoritmo voraz para elegir elementos

CuandopruebaCodiciosos()se ejecuta se imprime


Use codicioso por valor para llenar la mochila de
tamaño 20 Valor total de los artículos tomados =
200.0
<computadora, 200.0, 20.0>

Use codicioso por peso para llenar la mochila de


tamaño 20 Valor total de los artículos tomados =
170.0
<libro, 10.0, 1.0>
<florero, 50.0, 2.0>
<radio, 20.0, 4.0>
<pintura, 90.0, 9.0>

Use codicioso por densidad para llenar mochila de


tamaño 20 Valor total de artículos tomados =
255.0
<florero, 50.0, 2.0>
<reloj, 175.0, 10.0>
<libro, 10.0, 1.0>
<radio, 20.0, 4.0>

¿Cuá l es la eficiencia algorítmica de greedy? Hay dos cosas a considerar: la


complejidad de tiempo de la funció n integrada ordenada y el nú mero de veces a
través del bucle for en el cuerpo de greedy. El nú mero de iteraciones del ciclo está
limitado por el nú mero de elementos en los elementos, es decir, es O(n), donde n es
la longitud de los elementos. Sin embargo, el peor momento para la clasificació n
integrada de Python
2 Capítulo 17. Problemas de optimización de gráfico y

La funció n es aproximadamente O(n log n), donde n es la longitud de la lista a


ordenar111. Por lo tanto, el tiempo de ejecució n de greedy es O (n log n).

17.1.2 Una solución óptima al problema de la mochila 0/1


Supongamos que decidimos que una aproximació n no es lo suficientemente buena,
es decir, queremos la mejor solució n posible para este problema. Tal solució n se
llama ó ptima, lo que no sorprende ya que estamos resolviendo un problema de
optimizació n. Da la casualidad de que este es un ejemplo de un problema de
optimizació n clá sico, llamado problema de la mochila 0/1.

El problema de la mochila 0/1 se puede formalizar de la siguiente manera:

1. Cada elemento está representado por un par, <valor, peso>.


2. La mochila puede acomodar artículos con un peso total de no má s dew.
3. Un vector, I, de longitud n, representa el conjunto de elementos disponibles.
Cada elemento del vector es un elemento.
4. Se usa un vector, V, de longitud n, para indicar si el ladró n se lleva o no cada
artículo. Si V[i] = 1, se toma el elemento I[i]. Si V[i] = 0, no se toma el elemento
I[i].
5. Encuentre una V que maximice
norte1

V[i]* I[i].valor
i 0

sujeto a la restricció n de que


norte1

V[i]* I[i].pesoc w
i 0

Veamos qué sucede si tratamos de implementar esta formulació n del


problema de una manera sencilla:

1. Enumere todas las combinaciones posibles de elementos. Es decir, generar


todos los subconjuntos112del conjunto de elementos. A esto se le llama el
conjunto potencia y se discutió en el Capítulo 9.
2. Eliminar todas las combinaciones cuyo peso supere el peso permitido.
3. De las combinaciones restantes elige cualquiera cuyo valor sea el mayor.
Este enfoque ciertamente encontrará una respuesta ó ptima. Sin embargo, si el
conjunto original de elementos es grande, llevará mucho tiempo ejecutarlo porque,
como vimos en el Capítulo 9, la cantidad de subconjuntos crece con mucha rapidez
con la cantidad de elementos.

La figura 17.4 contiene una implementació n directa de este enfoque de fuerza bruta
para resolver el problema de la mochila 0/1. Utiliza las clases y funciones definidas
en la Figura 17.2 y la Figura 17.3, y la funció n genPowerset definida en la Figura
9.5.

111Como discutimos en el Capítulo 10, la complejidad temporal del algoritmo de clasificació n,


timsort, utilizadoen la mayoría de las implementaciones de Python es O(n log n).
112Recuerda que todo conjunto es un subconjunto de sí mismo y el conjunto vacío es un
subconjunto de todo conjunto.
Capítulo 17. Problemas de optimización de gráfico y 2

def elegirMejor(pset, maxWeight, getVal, getWeight):


bestVal = 0.0
bestSet = Ninguno
para elementos en
pset:
elementosVal = 0.0
itemsWeight = 0.0
para artículo en
artículos:
itemsVal += getVal(elemento)
itemsWeight +=
getWeight(elemento)
if itemsWeight <= maxWeight and itemsVal > bestVal:
bestVal = itemsVal
bestSet =
devolución de artículos
(bestSet, bestVal)

def testBest(maxWeight =
20): items =
buildItems()
pset = genPowerset(elementos)

Figura 17.4 Solución óptima de fuerza bruta al problema de la mochila 0/1

elmila complejidad de esta implementació n es O(n*2n), donde n es la longitud de los elementos.


La funció n genPowerset devuelve una lista de listas de elementos. Esta lista tiene una longitud
de 2n, y la lista má s larga tiene una longitud de n. Por lo tanto, el ciclo externo en elegirMejor se
ejecutará O(2n)) veces, y el nú mero de veces que se ejecutará el ciclo interno está limitado por n.

Se pueden aplicar muchas pequeñ as optimizaciones para acelerar este


programa. Por ejemplo, genPowerset podría haber tenido el encabezado
def genPowerset(elementos, restricción, getVal, getWeight)

y devolvió solo aquellas combinaciones que cumplen con la restricció n de peso.


Alternativamente, elijaMejor podría salir del ciclo interno tan pronto como se
exceda la restricció n de peso. Si bien este tipo de optimizaciones a menudo vale la
pena, no abordan el problema fundamental. La complejidad de elegirLa mejor
voluntadseguirá siendo O(n*2n), donde n es la longitud de los elementos y, por lo tanto,
elegirMejor aú n tardará mucho tiempo en ejecutarse cuando los elementos sean grandes.

En un sentido teó rico, el problema no tiene solució n. El problema de la mochila


0/1 es inherentemente exponencial en el nú mero de artículos. Sin embargo, en un
sentido prá ctico, el problema está lejos de ser irremediable, como veremos en el
Capítulo 18.

Cuandopruebamejorse ejecuta, se imprime,


Valor total de los artículos tomados = 275.0
<reloj, 175.0, 10.0>
<pintura, 90.0, 9.0>
<libro, 10.0, 1.0>

Tenga en cuenta que esta solució n es mejor que cualquiera de las soluciones encontradas
por los algoritmos codiciosos. La esencia de un algoritmo codicioso es hacer lo mejor (segú n
lo definido por
2 Capítulo 17. Problemas de optimización de gráfico y

alguna métrica) elecció n local en cada paso. Hace una elecció n que es
localmente ó ptima. Sin embargo, como ilustra este ejemplo, una serie de
decisiones localmente ó ptimas no siempre conduce a una solució n
globalmente ó ptima.

A pesar de que no siempre encuentran la mejor solució n, en la prá ctica se utilizan a


menudo algoritmos codiciosos. Por lo general, son má s fá ciles de implementar y má s
eficientes de ejecutar que los algoritmos que garantizan encontrar soluciones
ó ptimas. Como dijo una vez Ivan Boesky: “Creo que la codicia es saludable. Puedes
ser codicioso y aun así sentirte bien contigo mismo”. 113

Existe una variante del problema de la mochila, llamada problema de la mochila


fraccional (o continuo), para el cual se garantiza que un algoritmo codicioso
encontrará una solució n ó ptima. Dado que los artículos son infinitamente divisibles,
siempre tiene sentido tomar la mayor cantidad posible del artículo con la relació n
valor-peso restante má s alta. Supongamos, por ejemplo, que nuestro ladró n
encuentra solo tres cosas de valor en la casa: un saco de polvo de oro, un saco de
polvo de plata y un saco de pasas. En este caso, un algoritmo voraz por densidad
siempre encontrará la solució n ó ptima.

17.2 Problemas de optimización de gráficos


Pensemos en otro tipo de problema de optimizació n. Suponga que tiene una lista
de los precios de todos los vuelos de aerolíneas entre cada par de ciudades de los
Estados Unidos. Suponga también que para todas las ciudades, A, B y C, el costo de
volar de A a C pasando por B era el costo de volar de A a B má s el costo de volar de
B a C. Algunas preguntas que podría hacer como preguntar son:

 ¿Cuá l es el menor nú mero de paradas entre un par de ciudades?


 ¿Cuá l es la tarifa aérea má s econó mica entre un par de ciudades?
 ¿Cuá l es el pasaje aéreo má s econó mico entre un par de ciudades que
implica no má s de dos paradas?
 ¿Cuá l es la forma menos costosa de visitar una colecció n de ciudades?
Todos estos problemas (y muchos otros) se pueden formalizar fá cilmente como
problemas de grá ficos.

Un grá fico114es un conjunto de objetos llamados nodos (o vé rtices) conectados por


un conjunto de aristas (o arcos). Si los bordes son unidireccionales, el grá fico se
llama grá fico dirigido o dígrafo. En un grafo dirigido, si hay una arista desde n1an2,
nos referimos an1como el nodo de origen o principal yn2como destino o nodo
secundario.

113Dijo esto, entre aplausos entusiastas, en un discurso de graduació n de 1986 en la


Escuela de Negocios de la Universidad de California en Berkeley. Unos meses má s tarde
fue acusado de trá fico de informació n privilegiada, un cargo que condujo a dos añ os de
prisió n y una multa de $100,000,000.
114Los informá ticos y los matemá ticos usan la palabra “grá fico” en el sentido que se le da
en este libro. Por lo general, usan la palabra "trazar" para denotar el tipo de grá ficos que
vimos en los capítulos 11-16.
Capítulo 17. Problemas de optimización de gráfico y 2

Los grá ficos se utilizan normalmente para representar situaciones en las que
existen relaciones interesantes entre las partes. El primer uso documentado de
grá ficos en matemá ticas fue en 1735 cuando el matemá tico suizo Leonhard
Euler utilizó lo que se conoce como teoría de grá ficos para formular y resolver el
problema de los puentes de Kö nigsberg.

Kö nigsberg, entonces la capital de Prusia Oriental, se construyó en la intersecció n


de dos ríos que contenían varias islas. Las islas estaban conectadas entre sí y con el
continente por siete puentes, como se muestra en el mapa a continuació n. Por
alguna razó n, los habitantes de la ciudad estaban obsesionados con la cuestió n de si
era posible realizar una caminata que cruzara cada puente exactamente una vez.

La gran idea de Euler fue que el problema podría simplificarse enormemente al ver
cada masa de tierra separada como un punto (piense en un "nodo") y cada puente
como una línea (piense en un "borde") que conecta dos de estos puntos. El mapa de
la ciudad podría entonces ser representado por el grá fico a la derecha del mapa.
Euler luego razonó que si un camino atravesara cada borde exactamente una vez,
debería ser el caso de que cada nodo en el medio del camino (es decir, cualquier
nodo excepto el primero y el ú ltimo nodo visitado) debe tener un nú mero par de
bordes para que está conectado. Dado que ninguno de los nodos de este grá fico
tiene un nú mero par de aristas, Euler concluyó que es imposible atravesar cada

puente exactamente una vez.

Mapa deKönigsberg de Euler Las flechas del mapa


simplificado apuntan a los puentes

De mayor interés que el problema de los puentes de Kö nigsberg, o incluso el teorema


de Euler (que generaliza su solució n al problema de los puentes de Kö nigsberg), es la
idea de utilizar la teoría de grafos para ayudar a comprender los problemas.

Por ejemplo, solo se necesita una pequeñ a extensió n del tipo de grá fico utilizado por
Euler para modelar el sistema de carreteras de un país. Si se asocia un peso con cada
borde de un grá fico (o dígrafo), se denomina grá fico ponderado. Usando grá ficos
ponderados, el sistema de carreteras se puede representar como un grá fico en el
que las ciudades está n representadas por nodos y las carreteras que las conectan
como bordes, donde cada borde está etiquetado con la distancia entre los dos nodos.
Má s generalmente, uno
2 Capítulo 17. Problemas de optimización de gráfico y

puede representar cualquier hoja de ruta (incluidas las que tienen calles de un
solo sentido) mediante un dígrafo ponderado.

De manera similar, la estructura de la World Wide Web se puede representar como


un dígrafo en el que los nodos son pá ginas web y hay un borde del nodo A al nodo B
si y solo si hay un enlace a la pá gina B en la pá gina A. Patrones de trá fico podría
modelarse agregando un peso a cada borde que indique con qué frecuencia se usa.

También hay muchos usos menos obvios de los grá ficos. Los bió logos usan grá ficos
para modelar cosas que van desde la forma en que las proteínas interactú an entre sí
hasta las redes de expresió n génica. Los físicos usan grá ficos para describir las
transiciones de fase. Los epidemió logos usan grá ficos para modelar trayectorias de
enfermedades. Etcétera.

La figura 17.5 contiene clases que implementan tipos abstractos correspondientes a


nodos, aristas ponderadas y aristas.

Tener una clase para los nodos puede parecer una exageració n. Después de todo,
ninguno de los métodos de la clase Node realiza ningú n cá lculo interesante.
Introdujimos la clase simplemente para darnos la flexibilidad de decidir, quizá s en
algú n momento posterior, introducir una subclase de Nodo con propiedades
adicionales.
Nodo de clase (objeto):Figura 17.5 Nodos y aristas
def __init__(self, nombre):
"""Asume que el nombre es
una cadena""" self.name =
nombre
def getName(self):
return
self.nombre
def __str__(self):
return
self.nombre

borde de clase (objeto):


def __init__(self, src, dest):
"""Supone que src y dest son
nodos""" self.src = src
self.dest =
dest def
getSource(self):
devolver self.src
def
getDestination(self
): return self.dest
def __str__(uno mismo):
devuelve self.src.getName() + '->' + self.dest.getName()

clase borde ponderado (borde):


def __init__(self, src, dest, peso = 1.0):
"""Asume que src y dest son nodos, pondera un float"""
self.src = src
self.destino =
destino self.peso
= peso
Capítulo 17. Problemas de optimización de gráfico y 2

La figura 17.6 contiene implementaciones de las clases Digraph y Graph. Una


decisió n importante es la elecció n de la estructura de datos utilizada para
representar un dígrafo. Una representació n comú n es una matriz de adyacencia n
× n, donde n es el nú mero de nodos en el grá fico. Cada celda de la matriz contiene
informació n (p. ej., pesos) sobre los bordes que conectan el par de nodos <i, j>. Si
los bordes no está n ponderados, cada entrada es verdadera si y solo si hay un
borde de i a j.

Otra representació n comú n es una lista de adyacencia, que usamos aquí. Class
Digraph tiene dos variables de instancia. Los nodos variables son una lista de
Python que contiene los nombres de los nodos en el Digraph. La conectividad de los
nodos se representa mediante una lista de adyacencia implementada como un
diccionario. Los bordes variables son un diccionario que mapea cada Nodo en el
Digraph a una lista de los hijos de ese Nodo.
Class Graph es una subclase de Digraph. Hereda todos los métodos de
Digraph excepto addEdge, que anula. (Esta no es la forma más
eficiente en cuanto a espacio para implementar Graph, ya que almacena
cada borde dos veces, una para cada dirección en Digraph. Pero tiene
la virtud de la simplicidad).

clase Digraph(objeto):
#nodes es una lista de los nodos en el gráfico
#edges es un dictado que asigna cada nodo a una lista de sus
hijos def __init__(self):
self.nodos = []
self.bordes =
{}
def addNode(self, node):
if node in self.nodes:
aumentar ValueError('Nodo
duplicado') de lo contrario:
self.nodes.append(nodo)
self.edges[nodo] = []
def addEdge(self, edge):
src = edge.getSource()
destino = borde.obtenerDestino()
si no (src en self.nodes y dest en self.nodes):
aumentar ValueError('Node not in graph')
self.edges[src].append(destino
) def childrenOf(self, nodo):
return
self.bordes[nodo] def
hasNode(self, nodo):
devolver nodo en
self.nodes def __str__(self):
resultado = ''
para src en self.nodes:
para destino en self.edges[src]:
resultado = resultado + src.getName() + '->'\
+ dest.getName() + '\n'
devuelve resultado[:-1] #omitir nueva
línea final

gráfico de clase (dígrafo):


def addEdge(self, edge):
Digraph.addEdge(self, edge)
rev = Edge(edge.getDestination(), edge.getSource())
Digraph.addEdge(self, rev)

Figura 17.6 ClasesGraficoyDígrafo


2 Capítulo 17. Problemas de optimización de gráfico y

Es posible que desee detenerse un minuto y pensar por qué Graph es una subclase
de Digraph, y no al revé s. En muchos de los ejemplos de subclases que hemos visto,
la subclase agrega atributos a la superclase. Por ejemplo, la clase WeightedEdge
agregó un atributo de peso a la clase Edge.

Aquí, Digraph y Graph tienen los mismos atributos. La ú nica diferencia es la


implementació n del mé todo addEdge. Cualquiera podría haberse implementado
fá cilmente al heredar mé todos del otro, pero la elecció n de cuá l hacer la superclase no
fue arbitraria. En el Capítulo 8 enfatizamos la importancia de obedecer el principio de
sustitució n: si el có digo del cliente funciona correctamente usando una instancia del
supertipo, también debería funcionar correctamente cuando una instancia del subtipo
se sustituye por la instancia del supertipo.

Y, de hecho, si el có digo del cliente funciona correctamente usando una instancia de


Digraph, funcionará correctamente si se sustituye una instancia de Graph por la
instancia de Digraph. Lo contrario no es cierto. Hay muchos algoritmos que
funcionan en grá ficos (explotando la simetría de los bordes) que no funcionan en
grá ficos dirigidos.

17.2.1 Algunos problemas clásicos de teoría de grafos


Una de las cosas buenas de formular un problema utilizando la teoría de grafos es
que existen algoritmos bien conocidos para resolver muchos problemas de
optimizació n en grá ficos. Algunos de los problemas de optimizació n de grafos má s
conocidos son:

 Camino más corto. Para algú n par de nodos,N1yN2, encuentre la


secuencia má s corta de aristas<s, d>(nodo de origen y nodo de destino), tal
que nn

o El nodo de origen en el primer borde es N1


o El nodo de destino del ú ltimo borde esN2
o Para todos los bordes e1 y e2 en la secuencia, si e2 sigue a e1 en
la secuencia, el nodo de origen de e2 es el nodo de destino de e1.
i

 Ruta ponderada más corta. Esto es como el camino má s corto, excepto que
en lugar de elegir la secuencia de aristas má s corta que conecta dos nodos,
definimos alguna funció n sobre los pesos de las aristas en la secuencia (por
ejemplo, su suma) y minimizamos ese valor. Este es el tipo de problema que
solucionan Mapquest y Google Maps cuando se les pide que calculen las
direcciones de conducció n entre dos puntos.
 camarillas. Encuentre un conjunto de nodos tal que haya una ruta (o, a
menudo, una ruta que no exceda una longitud má xima) en el grá fico entre
cada par de nodos en el conjunto.115
 corte mínimo. Dados dos conjuntos de nodos en un grá fico, un corte es
un conjunto de bordes cuya eliminació n elimina todos los caminos de
cada nodo en un conjunto a cada nodo en el otro. El corte mínimo es el
conjunto má s pequeñ o de bordes cuya eliminació n logra esto.

115Estanoció n es bastante similar a la noció n de camarilla social, es decir, un grupo de


personas que se sienten estrechamente conectadas entre sí y tienden a excluir a quienes
no pertenecen a la camarilla. Vé ase, por ejemplo, la película Heathers.
Capítulo 17. Problemas de optimización de gráfico y 2

17.2.2 La propagación de enfermedades y el corte mínimo


La figura 17.7 contiene una representació n pictó rica de un grá fico ponderado
generado por los Centros para el Control de Enfermedades (CDC) de los EE. UU.
durante el estudio de un brote de tuberculosis en los Estados Unidos. Cada nodo
representa a una persona, y cada nodo está etiquetado con un color116 que indica si
la persona tiene TB activa, dio positivo en la prueba de exposició n a la TB (es decir,
alta tasa de reacció n a la TST), dio negativo en la prueba de exposició n a la TB o no
se le hizo la prueba. Los bordes representan el contacto entre parejas de personas.
Los pesos, que no se ven en la imagen, indican si el contacto entre las personas fue

“cercano” o “casual”.

Figura 17.7 Propagación de la tuberculosis

Hay muchas preguntas interesantes que se pueden formalizar utilizando este


grá fico. Por ejemplo,

 ¿Es posible que todos los casos provinieran de un solo paciente “índice”?
Má s formalmente, ¿hay un nodo,norte, tal que hay un camino desdenortea
todos los demá s nodos del grá fico con una etiqueta de TB activa?117La
respuesta es “casi”. Hay una ruta desde el nodo en el medio del grá fico
hasta cada nodo de TB activo, excepto los nodos en el círculo negro de la
derecha. Curiosamente, la investigació n posterior reveló que la persona
en el centro del círculo negro había sido previamente un vecino del
supuesto paciente índice y, por lo tanto, debería haber habido un borde
de contacto casual que vinculara a los dos.

116Paraver una versió n en color de este grá fico, vaya a la pá gina


23 dehttp://www.orgnet.com/TB_web.ppt
117Los bordes del grá fico no capturan nada relacionado con el tiempo. Por lo tanto, la
existencia de tal nodo no significa que el nodo represente un paciente índice. Sin embargo,
la ausencia de tal nodo indicaría la ausencia de un paciente índice. Tenemos una
condició n necesaria, pero no suficiente.
2 Capítulo 17. Problemas de optimización de gráfico y

 Para limitar mejor la propagació n continua, ¿qué personas no infectadas


deben vacunarse? Esto se puede formalizar como la solució n de un
problema de corte mínimo. DejarN / Aser el conjunto de nodos TB activos
yNOsea el conjunto de todos los demá s nodos. Cada borde en el corte
mínimo entre estos dos conjuntos contendrá una persona con TB activa
conocida y una persona sin TB. Las personas sin TB activa conocida son
candidatas a vacunació n.

17.2.3 Ruta más corta: búsqueda primero en profundidad y búsqueda


primero en amplitud
Las redes sociales está n formadas por individuos y relaciones entre individuos. Por
lo general, se modelan como grá ficos en los que los individuos son nodos y los
bordes son relaciones. Si las relaciones son simétricas, los bordes no está n dirigidos;
si las relaciones son asimétricas los bordes está n dirigidos. Algunas redes sociales
modelan mú ltiples tipos de relaciones, en cuyo caso las etiquetas en los bordes
indican el tipo de relació n.

En 1990118 el dramaturgo John Guare escribió Seis grados de separació n. La


premisa ligeramente dudosa que subyace en la obra es que "todos en este planeta
está n separados por solo otras seis personas". Con esto quiso decir que si uno
construyera una red social que incluyera a todas las personas en la tierra usando la
relació n "sabe", el camino má s corto entre dos individuos pasaría como má ximo a
través de otros seis nodos.

Una pregunta menos hipoté tica es la distancia usando la relació n de “amigo” entre
pares de personas en Facebook. Por ejemplo, puede preguntarse si tiene un amigo
que tiene un amigo que tiene un amigo que es amigo de Mick Jagger. Pensemos en
diseñ ar un programa para responder a tales preguntas.

La relació n de amistad (al menos en Facebook) es simé trica, por ejemplo, si


Stephanie es amiga de Andrea, Andrea es amiga de Stephanie. Por lo tanto,
implementaremos la red social utilizando el tipo Graph. Entonces podemos
definir el problema de encontrar la conexió n má s corta entre usted y Mick
Jagger como:

 para el grá ficoGRAMO, encuentre la secuencia má s corta de nodos,


camino = [Tú,…,Mick Jagger] , tal que

 Sinoyni+1son nodos consecutivos encamino, hay una ventaja enGRAMO


conectandonoyni+1.
La figura 17.8 contiene una funció n recursiva que encuentra el camino má s corto
entre dos nodos, inicio y final, en un dígrafo. Dado que Graph es una subclase de
Digraph, funcionará para nuestro problema de Facebook.

El algoritmo implementado por DFS es un ejemplo de un algoritmo recursivo de


bú squeda en profundidad (DFS). En general, un algoritmo de bú squeda primero en
profundidad comienza eligiendo un hijo del nodo de inicio. Luego elige un hijo de
ese nodo y así sucesivamente, profundizando má s y má s hasta que alcanza el nodo
objetivo o un nodo sin hijos. Luego, la bú squeda retrocede y regresa al nodo má s
reciente con niñ os que aú n no ha visitado. Cuando todos los caminos han sido

118Cuando Mark Zuckerberg tenía seis añ os.


Capítulo 17. Problemas de optimización de gráfico y 2

explorado, elige el camino má s corto (suponiendo que lo haya) desde el principio


hasta la meta.

El có digo es un poco má s complicado que el algoritmo que acabamos de describir


porque tiene que lidiar con la posibilidad de que el grá fico contenga ciclos. También
evita explorar caminos má s largos que el camino má s corto que ya ha encontrado.

 La funció nbuscarllamadasSFDconcamino = [](para indicar que la ruta actual


que se está explorando está vacía) ymás corto = Ninguno(para indicar que no
hay camino desdecomenzarafinaú n no se ha encontrado).
 SFDcomienza eligiendo un hijo de comenzar. Luego elige un hijo de ese
nodo y así sucesivamente, hasta que llega al nodo fino un nodo sin hijos no
visitados.
o el cheque
si el nodo no está en la ruta
evita que el programa quede atrapado en un ciclo.
o el cheque
si el más corto == Ninguno o len (ruta) < len (más corto) :
se utiliza para decidir si es posible que al continuar buscando en
esta ruta se obtenga una ruta má s corta que la mejor ruta
encontrada hasta el momento.

o En ese caso,SFDse llama recursivamente. Si encuentra un camino


afinque no es má s largo que el mejor encontrado hasta ahora, más
cortose actualiza

o Cuando el ú ltimo nodo encaminono tiene hijos para visitar, el


programa retrocede al nodo visitado previamente y visita el
siguiente hijo de ese nodo.
 La funció n regresa cuando todos los caminos posiblemente má s cortos
desdecomenzarafin
han sido explorados.

La figura 17.9 contiene có digo que ejecuta el


có digo de la figura. La funció n testSP en la
figura
17.9 primero crea un grá fico dirigido como el
que se muestra a la derecha y luego busca el
camino má s corto entre el nodo 0 y el nodo 5.
2 Capítulo 17. Problemas de optimización de gráfico y

def imprimirRuta(ruta):
"""Asume que la ruta es una lista
de nodos""" resultado = ''
para i en el rango (len (ruta)):
resultado = resultado +
str(ruta[i]) si i !=
len(ruta) - 1:
resultado =
resultado + '->' devolver
resultado

def DFS(gráfico, inicio, fin, ruta, más corto):


"""Asume que el gráfico es un dígrafo; el inicio y el
final son nodos; la ruta y el más corto son listas
de nodos
Devuelve la ruta más corta de principio a fin en el
gráfico""" ruta = ruta + [inicio]
imprimir 'Ruta DFS actual:',
printPath(ruta) if inicio == fin:
vía de retorno
para el nodo en
graph.childrenOf(start): si el
nodo no está en la ruta: #evitar
ciclos
if el más corto == Ninguno o len(ruta) < len(la más
corta): nuevaRuta = DFS(gráfico, nodo, final,
ruta, la más corta) if nuevaRuta != Ninguno:
más corto = nuevaRuta
volver más corto

Figura 17.8 Algoritmo de ruta más corta de búsqueda primero en


profundidad

Figura 17.9 Probar el código de búsqueda en profundidad


def pruebaSP(): primero
nodos = []
for name in range(6): #Crear 6
nodos
nodes.append(Node(str(name)))
g = Digraph()
para n en
nodos:
g.addNode(n)g.addEdge(Edge(no
dos[0],nodos[1]))
g.addEdge(Edge(nodos[1],nodos[2]))
g.addEdge(Edge(nodos[2],nodos[3])
)
g.addEdge(Edge(nodos[2],nodos[4]))
g.addEdge(Edge(nodos[3],nodos[4]))
g.addEdge(Edge(nodos[3],nodos[5] )
)
g.addEdge(Edge(nodos[0],nodos[2]))
g.addEdge(Edge(nodos[1],nodos[0]))
g.addEdge(Edge(nodos[3],nodos[1 ])
Capítulo 17. Problemas de optimización de gráfico y 2

Cuando se ejecuta, testSP produce la salida


Ruta DFS actual: 0 Ruta
DFS actual: 0->1 Ruta DFS
actual: 0->1->2
Ruta DFS actual: 0->1->2->3
Ruta DFS actual: 0->1->2->3->4
Ruta DFS actual: 0->1->2->3->5
DFS actual ruta: 0->1->2->4
Ruta DFS actual: 0->2
Ruta DFS actual: 0->2->3
Ruta DFS actual: 0->2->3->4
Ruta DFS actual: 0->2->3->5
Ruta DFS actual: 0->2->3 ->1
Ruta DFS actual: 0->2->4
Ruta más corta encontrada por DFS: 0->2->3->5

Observe que despué s de explorar la ruta 0->1->2->3->4, retrocede hasta el nodo 3 y


explora la ruta 0->1->2->3->5. Después de guardarlo como la ruta correcta má s corta
hasta el momento, retrocede hasta el nodo 2 y explora la ruta 0->1->2->4. Cuando llega
al final de esa ruta (nodo 4), retrocede hasta el nodo 0 e investiga la ruta comenzando
con el borde del 0 al 2. Y así sucesivamente.

El algoritmo DFS implementado anteriormente encuentra la ruta con el nú mero


mínimo de aristas. Si las aristas tienen pesos, no necesariamente encontrará el
camino que minimice la suma de los pesos de las aristas. Sin embargo, se modifica
fá cilmente para hacerlo.

Por supuesto, hay otras formas de recorrer un grá fico ademá s de la profundidad.
Otro enfoque comú n es la bú squeda primero en amplitud (BFS). En un recorrido
primero en anchura, primero se visitan todos los hijos del nodo de inicio. Si
ninguno de ellos es el nodo final, se visitan todos los hijos de cada uno de esos
nodos. Etcé tera. A diferencia de la bú squeda primero en profundidad, que
generalmente se implementa de forma recursiva, la bú squeda primero en
amplitud generalmente se implementa iterativamente. BFS explora muchas rutas
simultá neamente, agregando un nodo a cada ruta en cada iteració n. Dado que
genera los caminos en orden ascendente de longitud, se garantiza que el primer
camino encontrado con el objetivo como su ú ltimo nodo tenga un nú mero
mínimo de aristas.

La figura 17.10 contiene có digo que usa una bú squeda primero en anchura para
encontrar la ruta má s corta en un grá fico dirigido. La variable pathQueue se utiliza
para almacenar todas las rutas que se está n explorando actualmente. Cada iteració n
comienza eliminando una ruta de pathQueue y asignando esa ruta a tmpPath. Si el
ú ltimo nodo en tmpPath es end, se devuelve tmpPath. De lo contrario, se crea un
conjunto de rutas nuevas, cada una de las cuales extiende tmpPath agregando uno de
sus elementos secundarios. Luego, cada una de estas nuevas rutas se agrega a
pathQueue.
2 Capítulo 17. Problemas de optimización de gráfico y

def BFS(gráfico, inicio, final):


"""Asume que el gráfico es un dígrafo; el inicio y el final
son nodos Devuelve la ruta más corta desde el principio
hasta el final del gráfico"""
initPath = [start]
pathQueue = [initPath]
while len(pathQueue) !=
0:
#Obtenga y elimine el elemento más antiguo en
pathQueue tmpPath = pathQueue.pop(0)
print 'Ruta BFS actual:', printPath(tmpPath)
lastNode = tmpPath[-1]
si último nodo ==
final:
devuelve
tmpPath
para nextNode en
graph.childrenOf(lastNode): si nextNode
Figura 17.10 Ruta más corta de búsqueda primero en amplitud

cuando las lineas


sp = BFS(g, nodos[0], nodos[5])
imprimir 'Ruta más corta encontrada por BFS:', printPath(sp)

se agregan al final de testSP y la funció n se ejecuta imprime


Ruta DFS actual: 0 Ruta
DFS actual: 0->1 Ruta DFS
actual: 0->1->2
Ruta DFS actual: 0->1->2->3
Ruta DFS actual: 0->1->2->3->4
Ruta DFS actual: 0->1->2->3->5
DFS actual ruta: 0->1->2->4
Ruta DFS actual: 0->2
Ruta DFS actual: 0->2->3
Ruta DFS actual: 0->2->3->4
Ruta DFS actual: 0->2->3->5
Ruta DFS actual: 0->2->3 ->1
Ruta DFS actual: 0->2->4
Ruta más corta encontrada por DFS: 0->2-
>3->5 Ruta BFS actual: 0
Ruta BFS actual: 0->1 Ruta
BFS actual: 0->2 Ruta BFS
actual: 0->1->2 Ruta BFS
actual: 0->2->3 Ruta BFS
actual: 0->2->4 Ruta actual
Ruta BFS: 0->1->2->3 Ruta
BFS actual: 0->1->2->4 Ruta
BFS actual: 0->2->3->4 Ruta
BFS actual: 0->2- >3->5
Ruta más corta encontrada por BFS: 0->2->3->5
Capítulo 17. Problemas de optimización de gráfico y 2

Afortunadamente, cada algoritmo encontró un camino de la misma longitud. En


este caso, encontraron el mismo camino. Sin embargo, si un grá fico contiene má s
de una ruta má s corta entre un par de nodos, DFS y BFS no encontrará n
necesariamente la misma ruta má s corta.

Como se mencionó anteriormente, BFS es una forma conveniente de buscar una ruta
con la menor cantidad de bordes porque la primera vez que se encuentra una ruta,
se garantiza que será tal ruta.

Ejercicio de dedos:Considere un dígrafo con bordes ponderados. ¿Está


garantizado que el primer camino encontrado por BFS minimice la suma de los
pesos de los bordes?
18 PROGRAMACIÓN DINÁMICA

Programación dinámicaFue inventado por Richard Bellman a principios de la


dé cada de 1950. No intente inferir nada sobre la té cnica a partir de su nombre. Tal
como Bellman lo describió , el nombre “programació n diná mica” se eligió para
ocultar a los patrocinadores gubernamentales “el hecho de que realmente estaba
haciendo matemá ticas… [la frase programació n diná mica] era algo a lo que ni
siquiera un congresista podía objetar”. 119

La programació n diná mica es un mé todo para resolver de manera eficiente


problemas que exhiben las características de subproblemas superpuestos y
una subestructura ó ptima.
Afortunadamente, muchos problemas de optimizació n exhiben estas características.

Un problema tiene una subestructura ó ptima si se puede encontrar una solució n


ó ptima global combinando soluciones ó ptimas a subproblemas locales. Ya hemos
visto varios de estos problemas. La ordenació n por combinació n, por ejemplo,
aprovecha el hecho de que una lista se puede ordenar ordenando primero las
sublistas y luego fusionando las soluciones.

Un problema tiene subproblemas superpuestos si una solució n ó ptima implica


resolver el mismo problema varias veces. La ordenació n por combinació n no
muestra esta propiedad.
Aunque estamos realizando una fusió n muchas veces, estamos fusionando
diferentes listas cada vez.

No es inmediatamente obvio, pero el problema de la mochila 0/1 exhibe ambas


propiedades. Sin embargo, antes de ver eso, haremos una digresió n para ver un
problema donde la subestructura ó ptima y los subproblemas superpuestos son má s
obvios.

18.1 Secuencias de Fibonacci, revisadas


En el capítulo 4, vimos una implementació n recursiva directa de la funció n de
Fibonacci, que se muestra aquí en la figura 18.1.

Figura 18.1 Implementación recursiva de la función de


def fib(n):
Fibonacci
"""Asume que n es un int
>= 0 Devuelve Fibonacci
de n"""
si n == 0 o n ==
1: devuelve 1
demás:

119Como se cita en Stuart Dreyfus "Richard Bellman sobre el nacimiento de la programació n


diná mica",
La investigación de operaciones, vol. 50, nú m. 1 (2002).
Capítulo 18. Programación 2

Si bien esta implementació n de la recurrencia es obviamente correcta, es


terriblemente ineficiente. Intente, por ejemplo, ejecutar fib(120), pero no espere a
que se complete. La complejidad de la implementació n es un poco difícil de deducir,
pero es má s o menos O(fib(n)). Es decir, su crecimiento es proporcional al
crecimiento del valor del resultado, y la tasa de crecimiento de la secuencia de
Fibonacci es sustancial. Por ejemplo, fib(120) es
8,670,007,398,507,948,658,051,921. Si cada llamada recursiva tardara un
nanosegundo, fib(120) tardaría unos 250.000 añ os en finalizar.

Intentemos averiguar por qué esta implementació n lleva tanto tiempo. Dada la
pequeñ a cantidad de có digo en el cuerpo de fib, está claro que el problema debe
ser la cantidad de veces que fib se llama a sí mismo. Como ejemplo, observe el
á rbol de llamadas asociado con la invocació n fib(6).

cib(6)

cib(5) cib(4)

cib(4) cib(3) cib(3) cib(2)

cib(3) cib(2) cib(2)cib(1)cib(2)cib(1)cib(1)cib(0)

cib(2)cib(1)cib(1)cib(0)cib(1)cib(0)cib(1)cib(0)

cib(1)cib(0)

Figura 18.2 Árbol de llamadas para Fibonacci recursivo

Observe que estamos calculando los mismos valores una y otra vez. Por ejemplo,
fib se llama con 3 tres veces, y cada una de estas llamadas provoca cuatro
llamadas de fib adicionales. No hace falta ser un genio para pensar que sería una
buena idea registrar el valor devuelto por la primera llamada y luego buscarlo en
lugar de calcularlo cada vez que sea necesario. Esto se llama memorizació n y es
la idea clave detrá s de la programació n diná mica.

La figura 18.3 contiene una implementació n de Fibonacci basada en esta idea. La


funció n fastFib tiene un pará metro, memo, que utiliza para realizar un seguimiento
de los nú meros que ya ha evaluado. El pará metro tiene un valor predeterminado, el
diccionario vacío, para que los clientes de fastFib no tengan que preocuparse por
proporcionar un valor inicial para memo. Cuando se llama a fastFib con n > 1,
intenta buscar n en memo. Si no está allí (porque es la primera vez que se llama a
fastFib con ese valor), se genera una excepció n. Cuando esto sucede, fastFib usa la
recurrencia normal de Fibonacci y luego almacena el resultado en memo.
2 Capítulo 18. Programación

def fastFib(n, memo = {}):


"""Asume que n es un int >= 0, memo usado solo por llamadas
recursivas Devuelve Fibonacci de n"""
si n == 0 o n ==
1: devuelve 1
intentar:
devolver
memo[n] excepto
KeyError:
resultado = fastFib(n-1, memo) + fastFib(n-2,
memo) memo[n] = resultado

Figura 18.3 Implementando Fibonacci usando un memo

Si intenta ejecutar fastFib, verá que es bastante rá pido: fib(120) regresa casi
instantá neamente. ¿Cuá l es la complejidad de fastFib? Llama a fib exactamente
una vez para cada valor de 0 a n. Por lo tanto, bajo el supuesto de que la
bú squeda en el diccionario se puede realizar en tiempo constante, la complejidad
temporal de fastFib(n) es O(n).120

18.2 Programación Dinámica y el Problema de la Mochila 0/1


Uno de los problemas de optimizació n que vimos en el Capítulo 17 fue el problema
de la mochila 0/1. Recuerde que vimos un algoritmo codicioso que se ejecutaba en
n log n tiempo, pero no estaba garantizado encontrar una solución óptima. También analizamos un
algoritmo de fuerza bruta que garantizaba encontrar una solución óptima, pero se ejecutaba en un
tiempo exponencial. Finalmente, discutimos el hecho de que el problema es inherentemente
exponencial en el tamaño de la entrada. En el peor de los casos, uno no puede encontrar una
solución óptima sin mirar todas las respuestas posibles.

Afortunadamente, la situació n no es tan mala como parece. La programació n


diná mica proporciona un mé todo prá ctico para resolver la mayoría de los
problemas de mochila 0/1 en un tiempo razonable. Como primer paso para
derivar tal solució n, comenzamos con una solució n exponencial basada en una
enumeració n exhaustiva. La idea clave es pensar en explorar el espacio de
posibles soluciones mediante la construcció n de un á rbol binario enraizado que
enumere todos los estados que satisfacen la restricció n de peso.

Un á rbol binario con raíz es un grá fico dirigido acíclico en el que

 Hay exactamente un nodo sin padres. Esto se llama la raíz.


 Cada nodo no raíz tiene exactamente un padre.
 Cada nodo tiene como má ximo dos hijos. Un nodo sin hijos se llama hoja.

Cada nodo en el á rbol de bú squeda para el problema de la mochila 0/1 está


etiquetado con un cuá druple que denota una solució n parcial al problema de la
mochila.

120Aunquelindo y pedagó gicamente interesante, esta no es la mejor manera de


implementar Fibonacci. Hay una implementació n iterativa de tiempo lineal simple.
Capítulo 18. Programación 2

Los elementos del cuá druple son:

 Un conjunto de elementos a tomar,


 La lista de elementos para los que no se ha tomado una decisió n,
 El valor total de los artículos en el conjunto de artículos que se tomará n
(esto es simplemente una optimizació n, ya que el valor podría calcularse a
partir del conjunto), y
 El espacio restante en la mochila. (Nuevamente, esto es una optimizació n
ya que es simplemente la diferencia entre el peso permitido y el peso de
todos los artículos tomados hasta el momento).

El á rbol se construye de arriba hacia abajo comenzando con la raíz.121 Se selecciona


un elemento de los elementos que aú n deben considerarse. Si hay espacio para ese
artículo en la mochila, se construye un nodo que refleja la consecuencia de elegir
tomar ese artículo. Por convenció n, dibujamos ese nodo como el hijo izquierdo. El
niñ o correcto muestra las consecuencias de elegir no tomar ese artículo. Luego, el
proceso se aplica recursivamente hasta que la mochila está llena o no hay má s
artículos para considerar. Debido a que cada borde representa una decisió n (tomar o
no tomar un artículo), estos á rboles se denominan á rboles de decisió n.122

La figura 18.4 es una tabla que describe un conjunto de elementos. La figura


18.5 es un á rbol de decisió n para decidir cuá l de esos artículos tomar bajo el
supuesto de que la mochila tiene un peso má ximo de 5.

Nombre Valor Peso

a 6 3

b 7 3
C 8 2
d 9 5

Figura 18.4 Tabla de ítems con valores y pesos

121Puede parecer extrañ o poner la raíz de un á rbol en la parte superior, pero esa es la
forma en que los matemá ticos y los informá ticos suelen dibujarlos. Tal vez sea evidencia
de que esas personas no dedican suficiente tiempo a contemplar la naturaleza.
122Los á rboles de decisió n, que no necesitan ser binarios, proporcionan una forma
estructurada de explorar las consecuencias de tomar una serie de decisiones secuenciales.
Se utilizan ampliamente en muchos campos.
2 Capítulo 18. Programación

Figura 18.5 Árbol de decisión para el problema de la mochila

La raíz del á rbol (nodo 0) tiene una etiqueta <{}, [a,b,c,d], 0, 5>, que indica que no se
han tomado elementos, quedan todos los elementos por considerar, el valor de la
elementos tomados es 0, y todavía hay disponible un peso de 5. El nodo 1 indica que
se ha tomado a, [b, c, d] quedan por considerar, el valor de los artículos tomados es 6
y la mochila puede contener otras 2 libras. No hay ningú n nodo a la izquierda del
nodo 1, ya que el artículo b, que pesa 3 libras, no cabría en la mochila.

En la figura 18.5, los nú meros que preceden a los dos puntos en cada nodo indican
un orden en el que se pueden generar los nodos. Este orden particular se llama
primero a la izquierda primero en profundidad. En cada nodo intentamos generar
un nodo izquierdo. Si eso es imposible, intentamos generar un nodo derecho. Si eso
tambié n es imposible, hacemos una copia de seguridad de un nodo (al padre) y
repetimos el proceso. Eventualmente, nos encontramos generando todos los
descendientes de la raíz, y el proceso se detiene. Cuando el proceso se detiene, se ha
generado cada combinació n de elementos que podrían caber en la mochila, y
cualquier nodo hoja con el mayor valor representa una solució n ó ptima. Observe
que para cada nodo hoja,

Como era de esperar (especialmente si leyó el capítulo anterior), la


implementació n natural de una bú squeda de á rbol primero en profundidad es
recursiva. La figura 18.6 contiene una implementació n de este tipo. Utiliza la
clase Item de la Figura 17.2. La funció n maxVal devuelve dos valores, el
conjunto de elementos elegidos y el valor total de esos elementos. Se llama con
dos argumentos, correspondientes al segundo y cuarto elementos de las
etiquetas de los nodos en el á rbol:

 considerar. Aquellos elementos que nodos má s arriba en el á rbol


(correspondientes a llamadas anteriores en la pila de llamadas recursivas)
aú n no se han considerado.
 aprovechar. La cantidad de espacio aú n disponible.
Capítulo 18. Programación 2

Tenga en cuenta que la implementació n de maxVal no construye el á rbol de


decisió n y luego busca un nodo ó ptimo. En su lugar, utiliza el resultado de la
variable local para registrar la mejor solució n encontrada hasta el momento.

def maxVal(toConsider, disponibilidad):


"""Supone considerar una lista de elementos, aprovechar un
peso Devuelve una tupla del peso total de una solución a
la
0/1 problema de mochila y los elementos de esa
solución""" if toConsider == [] o aproveche == 0:
resultado = (0, ())
elif toConsider[0].getWeight() >
aproveche: #Explorar solo la rama
derecha
resultado = maxVal(aConsider[1:],
disponibilidad) más:
nextItem = toConsider[0]
#Explorar rama izquierda
withVal, withToTake = maxVal(toConsider[1:],
disponibilidad -
nextItem.getWeight()) withVal += nextItem.getValue()
#Explorar rama derecha
sinValor, sinTomar = maxVal(aConsider[1:],
aprovechar)
#Elegir mejor rama si
conVal > sinVal:
resultado = (withVal, withToTake + (nextItem,))
else:
resultado = (sinValor, sinTomar)
devuelve resultado

def pruebapequeña():
nombres = ['a', 'b', 'c',
'd'] val = [6, 7, 8, 9]
pesos = [3, 3, 2, 5]
Elementos = []
for i in range(len(vals)):
Items.append(Item(names[i], vals[i], weights[i]))
val, tomado =
maxVal(Artículos, 5) para el
artículo tomado:
elemento de impresión
print 'Valor total de los artículos tomados =', val

Figura 18.6 Uso de un árbol de decisión para resolver un


problema de mochila

Cuando se ejecuta smallTest (que utiliza los valores de la Figura 18.4), imprime un
resultado que indica que el nodo 8 de la Figura 18.5 es una solució n ó ptima:
<c, 8.0, 3.0>
<b, 7.0, 2.0>
Valor total de los artículos tomados = 15.0

Si ejecuta este có digo en cualquiera de los ejemplos que hemos visto, encontrará
que produce una respuesta ó ptima. De hecho, siempre producirá una respuesta
ó ptima, si llega a producir alguna respuesta.

El có digo de la figura 18.7 facilita la prueba de maxVal. Genera aleatoriamente una


lista de artículos de un tamañ o específico. Prueba con bigTest(10). Ahora intenta
2 Capítulo 18. Programación

prueba grande(40). Después de que te canses de esperar a que


regrese, detente y pregúntate qué está pasando.

Figura 18.7 Prueba de la implementación basada en árboles de


decisión
def buildManyItems(numItems, maxVal, maxWeight):
items = []
Pensemos en el tamañ o del á rbol que estamos explorando. Dado que en cada nivel
for i in range(numItems):
del á rbol decidimos conservar o no un elemento, la profundidad má xima del á rbol
items.append(Item(str(i),
es len(elementos). En el nivel 0 tenemos un solo nodo,maxVal),
random.randint(1, en el nivel 1 hasta dos nodos,
en el nivel 2 hasta cuatro nodos, en random.randint(1,
el nivel 3 hasta ocho nodos. En el nivel 39
maxWeight)))
tenemos hasta 239 nodos. ¡No es de extrañ ar que tarde mucho tiempo en funcionar!
devolver los artículos
¿Que debemos hacer sobre esto? Comencemos preguntando si este programa tiene
def bigTest(numElementos):
algo en artículos
comú n con nuestra primera implementació10,
= buildManyItems(numItems, n de Fibonacci. En particular,
¿existe una
10) subestructura
val, tomado =ó ptima y subproblemas
maxVal(items, 40) superpuestos?
imprime
La subestructura
'Artículosó ptima es visible tanto en la Figura 18.5 como en la Figura 18.6.
Cada nodo padre para
tomados' combina
el las soluciones alcanzadas por sus hijos para derivar
artículo
una solució tomado:
n ó ptima para el subá rbol enraizado en ese padre. Esto se refleja en la
figura 18.6 mediante el có digo que sigue al comentario #Choose better branch.

¿Hay tambié n subproblemas superpuestos? A primera vista, la respuesta parece


ser “no”. En cada nivel del á rbol tenemos un conjunto diferente de elementos
disponibles para considerar. Esto implica que si existen subproblemas comunes,
deben estar en el mismo nivel del á rbol. Y, de hecho, en cada nivel del á rbol, cada
nodo tiene el mismo conjunto de elementos que considerar tomar. Sin embargo,
al observar las etiquetas en la Figura 18.5, podemos ver que cada nodo en un
nivel representa un conjunto diferente de opciones sobre los elementos
considerados má s arriba en el á rbol.

Piense en qué problema se está resolviendo en cada nodo. El problema que se está
resolviendo es encontrar los elementos ó ptimos para tomar de los que quedan por
considerar, dado el peso disponible restante. El peso disponible depende del peso
total de los artículos tomados, pero no de los artículos tomados o del valor total de
los artículos tomados. Entonces, por ejemplo, en la Figura 18.5, los nodos 2 y 7 en
realidad está n resolviendo el mismo problema: decidir qué elementos de [c,d]
deben tomarse, dado que el peso disponible es 2.
Capítulo 18. Programación 2

El có digo de la figura 18.8 aprovecha la subestructura ó ptima y los subproblemas


superpuestos para proporcionar una solució n de programació n diná mica al
problema de la mochila 0/1. Se ha agregado un pará metro adicional, memo, para
realizar un seguimiento de las soluciones a los subproblemas que ya se han resuelto.
Se implementa mediante un diccionario con una clave construida a partir de la
longitud de toConsider y el peso disponible. La expresió n len(toConsider) es una
forma compacta de representar los elementos que quedan por considerar. Esto
funciona porque los elementos siempre se eliminan del mismo extremo (el frente)
de la lista para considerar.

Figura 18.8 Solución de programación dinámica al problema de la mochila


def fastMaxVal(toConsider, aproveche, memo = {}):
"""Supone
La figura considerar
18.9 muestra unadelista
el nú mero de artículos,
llamadas aprovechar
realizadas cuando ejecutamos el
una nota de peso utilizada solo por llamadas
có digo en problemas de
recursivas
varios tamañ os.
Devuelve una tupla del peso total de una solución al
problema de la mochila 0/1 y los elementos de esa
solución"""
if (len(paraConsiderar), disponible) en
nota: resultado =
nota[(len(paraConsiderar),
disponible)]
elif para considerar == [] o
disponibilidad == 0: resultado
= (0, ())
elif toConsider[0].getWeight() >
aproveche: #Explorar solo la rama
derecha
resultado = fastMaxVal(aConsider[1:],
disponibilidad, memo) otra cosa:
nextItem =
toConsider[0]
#Explorar rama
izquierda withVal,
withToTake =\
fastMaxVal(para considerar[1:],
disponibilidad -
nextItem.getWeight(), memo) withVal +=
nextItem.getValue()
#Explorar rama derecha
sinValor, sinTomar = fastMaxVal(aConsider[1:],
2 Capítulo 18. Programación

len(Artículos) Número de items Número de


seleccionado llamadas
4 4 31

8 6 337

dieciséis 9 1,493

32 12 3,650

64 19 8,707

128 27 18.306

256 40 36,675

Figura 18.9 Rendimiento de la solución de programación


dinámica

El crecimiento es difícil de cuantificar, pero claramente es mucho menos que


exponencial.123 Pero, ¿có mo puede ser esto, dado que sabemos que el problema
de la mochila 0/1 es inherentemente exponencial en el nú mero de artículos?
¿Hemos encontrado una manera de anular las leyes fundamentales del
universo? No, pero hemos descubierto que la complejidad computacional puede
ser una noció n sutil.124

El tiempo de ejecució n de fastMaxVal se rige por el nú mero de distintas


<toConsider, aproveche> pares generados. Esto se debe a que la
decisión sobre qué hacer a continuación depende únicamente de los
elementos que aún están disponibles y del peso total de los elementos
que ya se han tomado.

El nú mero de valores posibles deconsiderarestá delimitado porlen(artículos).

El nú mero de posibles valores de disponibilidad es má s difícil de caracterizar. Está


acotado desde arriba por el nú mero má ximo de totales distintos de pesos de los
artículos que puede contener la mochila. Si la mochila puede contener como má ximo
n artículos (segú n la capacidad de la mochila y los pesos de los artículos
disponibles), la disponibilidad puede tomar como má ximo 2n valores diferentes. En
principio, esto podría ser un nú mero bastante grande. Sin embargo, en la prá ctica, no
suele ser tan grande. Incluso si la mochila tiene una gran capacidad, si los pesos de
los artículos se eligen de un conjunto razonablemente pequeñ o de pesos posibles,
muchos conjuntos de artículos tendrá n el mismo peso total, lo que reducirá en gran
medida el tiempo de ejecució n.

Este algoritmo cae en una clase de complejidad llamada pseudo polinomio. Una
explicació n cuidadosa de este concepto está má s allá del alcance de este libro. En
té rminos generales, fastMaxVal es exponencial en la cantidad de bits necesarios
para representar los posibles valores de disponibilidad.

123Desde 2128 = 340.282.366.920.938.463.463.374.607.431.768.211.456


124OK,
"descubierto" puede ser una palabra demasiado fuerte. La gente sabe esto desde hace
mucho tiempo. Probablemente lo descubriste alrededor del Capítulo 9.
Capítulo 18. Programación 2

Para ver qué sucede cuando los valores de disponibilidad se eligen de un


espacio considerablemente má s grande, cambie la llamada a fastMaxVal en la
figura 18.7 para
val, tomado = fastMaxVal(artículos, 1000)

Encontrar una solució n ahora requiere1,802,817llamadas derápidoMaxValcuando el


nú mero de artículos es256.

Para ver qué sucede cuando los pesos se eligen de un espacio enorme, podemos
elegir los pesos posibles de los reales positivos en lugar de los enteros positivos.
Para hacer esto, reemplace la línea,
items.append(Item(str(i),
random.randint(1, maxVal),
random.randint(1, maxWeight)))

enconstruirMuchosItemspor la línea
items.append(Item(str(i),
random.randint(1, maxVal),
random.randint(1, maxWeight)*random.random()))

No aguantes la respiració n esperando a que termine esta ú ltima prueba. La


programació n diná mica puede ser una técnica milagrosa en el sentido comú n de la
palabra,125 pero no es capaz de realizar milagros en el sentido litú rgico.

18.3 Programación dinámica y divide y vencerás


Al igual que los algoritmos de divide y vencerá s, la programació n diná mica se
basa en resolver subproblemas independientes y luego combinar esas soluciones.
Hay, sin embargo, algunas diferencias importantes.

Los algoritmos de divide y vencerá s se basan en encontrar subproblemas que son


sustancialmente má s pequeñ os que el problema original. Por ejemplo, la ordenació n
por combinació n funciona dividiendo el tamañ o del problema por la mitad en cada
paso. Por el contrario, la programació n diná mica implica resolver problemas que
son solo un poco má s pequeñ os que el problema original. Por ejemplo, calcular el
nú mero 19 de Fibonacci no es un problema sustancialmente menor que calcular el
nú mero 20 de Fibonacci.

Otra distinció n importante es que la eficiencia de los algoritmos divide y vencerá s


no depende de la estructuració n del algoritmo para que los mismos problemas se
resuelvan repetidamente. En contraste, la programació n diná mica es eficiente solo
cuando el nú mero de subproblemas distintos es significativamente menor que el
nú mero total de subproblemas.

125Extraordinario y con consecuencias bienvenidas.


19 UNA MIRADA RÁPIDA AL APRENDIZAJE
AUTOMÁTICO

La cantidad de datos digitales en el mundo ha estado creciendo a un ritmo que


desafía la comprensió n humana. La capacidad mundial de almacenamiento de
datos se ha duplicado aproximadamente cada tres añ os desde la dé cada de
1980. Durante el tiempo que le llevará leer este capítulo, se agregará n
aproximadamente 1018 bits de datos a la tienda mundial. No es fá cil
relacionarse con un nú mero tan grande. Una forma de pensarlo es que 1018
centavos canadienses tendrían un á rea de superficie aproximadamente el doble
de la de la Tierra.

Por supuesto, má s datos no siempre conducen a informació n má s ú til. La


evolució n es un proceso lento y, por desgracia, la capacidad de la mente
humana para asimilar datos no se ha duplicado cada tres añ os. Un enfoque que
el mundo está utilizando para intentar explotar lo que se conoce como "grandes
datos" es el aprendizaje automá tico estadístico.

El aprendizaje automá tico es difícil de definir. Una de las primeras definiciones fue
propuesta por el ingeniero elé ctrico e informá tico estadounidense Arthur
Samuel,126 quien la definió como un “campo de estudio que otorga a las
computadoras la capacidad de aprender sin ser programadas explícitamente”. Por
supuesto, en cierto sentido, todo programa ú til aprende algo. Por ejemplo, una
implementació n del método de Newton aprende las raíces de un polinomio.

Los seres humanos aprenden cosas de dos maneras: memorizació n y


generalizació n. Usamos la memorizació n para acumular hechos individuales. En
Inglaterra, por ejemplo, los estudiantes de primaria pueden aprender una lista de
monarcas ingleses. Los humanos usan la generalizació n para deducir hechos
nuevos a partir de hechos antiguos. Un estudiante de ciencias políticas, por
ejemplo, podría observar el comportamiento de un gran nú mero de políticos y
generalizar para concluir que es probable que todos los políticos tomen decisiones
destinadas a mejorar sus posibilidades de permanecer en el cargo.

Cuando los informá ticos hablan de aprendizaje automá tico, con mayor frecuencia se
refieren al campo de la escritura de programas que aprenden automá ticamente a
hacer inferencias ú tiles a partir de patrones implícitos en los datos. Por ejemplo, la
regresió n lineal (consulte el Capítulo 15) aprende una curva que es un modelo de
una colecció n de ejemplos. Ese modelo se puede usar para hacer predicciones sobre
ejemplos nunca antes vistos.

En general, el aprendizaje automá tico implica observar un conjunto de ejemplos que


representan informació n incompleta sobre algú n fenó meno estadístico y luego
intentar inferir algo sobre el proceso que generó esos ejemplos. Los ejemplos se
denominan con frecuencia datos de entrenamiento.

126Samuel es probablemente mejor conocido como el autor del programa que jugaba a
las damas. El programa, en el que comenzó a trabajar en la dé cada de 1950 y continuó
trabajando en la dé cada de 1970, fue impresionante para su é poca, aunque no
particularmente bueno para los está ndares modernos. Sin embargo, mientras trabajaba
en é l, Samuel inventó varias té cnicas que todavía se utilizan en la actualidad. Entre otras
Capítulo 18. Programación 2

cosas, el programa de Samuel para jugar a las damas fue muy posiblemente el primer
programa jamá s escrito que mejoró en base a la "experiencia".
Capítulo 19. Una mirada rápida al 2

Suponga, por ejemplo, que le dieron los siguientes dos conjuntos de personas:
A: {Abraham Lincoln, George Washington, Charles de Gaulle}
B: {Benjamin Harrison, James Madison, Louis Napoleón}

Ahora, suponga que se le proporcionaron las siguientes descripciones parciales de


cada uno de ellos:
Abraham Lincoln: estadounidense, presidente, 193 cm
de altura George Washington: estadounidense,
presidente, 189 cm de altura Benjamin Harrison:
estadounidense, presidente, 168 cm de altura James
Madison: estadounidense, presidente, 163 cm de
altura Louis Napoleón: francés, presidente, 169 cm
de altura Charles de Gaulle: francés, presidente,
196 cm de altura

Con base en esta informació n incompleta sobre estas figuras histó ricas, podría
inferir que el proceso que asignó estos ejemplos al conjunto etiquetado como A o al
conjunto etiquetado como B implicó separar a los presidentes altos de los má s
bajos.

La informació n incompleta generalmente se denomina vector de características.


Cada elemento del vector describe algú n aspecto (es decir, característica) del
ejemplo.

Hay una gran cantidad de enfoques diferentes para el aprendizaje automá tico, pero
todos intentan aprender un modelo que es una generalizació n de los ejemplos
proporcionados. Todos tienen tres componentes:

 Una representació n del modelo,


 Una funció n objetiva para evaluar la bondad del modelo, y
 Un método de optimizació n para aprender un modelo que
minimiza o maximiza el valor de la funció n objetivo.

En té rminos generales, los algoritmos de aprendizaje automá tico pueden


considerarse supervisados o no supervisados.

En el aprendizaje supervisado, comenzamos con un conjunto de pares de vectores


de características/etiquetas.127 El objetivo es derivar de estos ejemplos una regla
que prediga la etiqueta asociada con un vector de características nunca antes visto.
Por ejemplo, dados los conjuntos A y B, un algoritmo de aprendizaje podría inferir
que todos los presidentes altos deben etiquetarse como A y todos los presidentes
bajos deben etiquetarse como B. Cuando se le pide que asigne una etiqueta a
Thomas Jefferson: estadounidense, presidente, 189 cm.

entonces elegiría la etiqueta A.

El aprendizaje automá tico supervisado se usa ampliamente en la prá ctica para


tareas como detectar el uso fraudulento de tarjetas de cré dito y recomendar
películas a las personas. Los mejores algoritmos son bastante sofisticados y
comprenderlos requiere un nivel de sofisticació n matemá tica mucho mayor que
el supuesto para este libro.
En consecuencia, no los cubriremos aquí.

127Gran parte de la literatura sobre aprendizaje automá tico utiliza la palabra "clase" en
lugar de "etiqueta". Dado que usamos la palabra "clase" para otra cosa en este libro, nos
2 limitaremos a usar "etiqueta" para este concepto.
Capítulo 19. Una mirada rápida al
Capítulo 19. Una mirada rápida al 2

En el aprendizaje no supervisado, se nos proporciona un conjunto de vectores de


funciones, pero no etiquetas. El objetivo del aprendizaje no supervisado es
descubrir la estructura latente en el conjunto de vectores de características. Por
ejemplo, dado el conjunto de vectores de características presidenciales, un
algoritmo de aprendizaje no supervisado podría separar a los presidentes en
altos y bajos, o quizá s en estadounidenses y franceses.

Las técnicas de aprendizaje no supervisado má s populares está n diseñ adas para


encontrar grupos de vectores de características similares. Los genetistas, por
ejemplo, utilizan la agrupació n para encontrar grupos de genes relacionados.
Muchos métodos populares de agrupamiento son sorprendentemente simples. Má s
adelante en este capítulo presentaremos el algoritmo má s utilizado.
Primero, sin embargo, queremos decir algunas palabras sobre la extracció n de
características.

19.1 Vectores de características


El concepto de relació n señ al-ruido (SNR) se utiliza en muchas ramas de la
ingeniería y la ciencia. La definició n precisa varía segú n las aplicaciones, pero la
idea bá sica es simple. Piense en ello como la relació n entre la entrada ú til y la
entrada irrelevante. En un restaurante, la señ al podría ser la voz de su cita para
cenar, y el ruido, las voces de los otros comensales.128 Si estuviéramos tratando de
predecir a qué estudiantes les iría bien en un curso de programació n, la experiencia
previa en programació n y la aptitud matemá tica serían parte de la señ al, pero
gé nero meramente ruido. Separar la señ al del ruido no siempre es fá cil. Y cuando se
hace mal, el ruido puede ser una distracció n que oscurece la verdad en la señ al.

El propó sito de la extracció n de características es separar aquellas características en


los datos disponibles que contribuyen a la señ al de aquellas que son meramente
ruido. El no hacer un trabajo adecuado de esto introduce dos tipos de problemas:

1. Las características irrelevantes pueden conducir a un mal modelo. El peligro


de esto es particularmente alto cuando la dimensionalidad de los datos (es
decir, el nú mero de características diferentes) es grande en relació n con el
nú mero de muestras.

2. Las características irrelevantes pueden ralentizar en gran medida el


proceso de aprendizaje. Los algoritmos de aprendizaje automá tico a
menudo son computacionalmente intensivos y la complejidad crece tanto
con la cantidad de ejemplos como con la cantidad de funciones.

El objetivo de la extracció n de características es reducir la gran cantidad de


informació n que podría estar disponible en los ejemplos a informació n a partir
de la cual será productivo generalizar. Imagine, por ejemplo, que su objetivo es
aprender un modelo que prediga si a una persona le gusta beber vino. Es
probable que algunos atributos, por ejemplo, la edad y la nació n en la que viven,
sean relevantes. Otros atributos, por ejemplo, si son zurdos, tienen menos
probabilidades de ser relevantes.

La extracció n de características es difícil. En el contexto del aprendizaje


supervisado, se puede intentar seleccionar aquellas características que se
correlacionan con las etiquetas de los ejemplos. En

128A menos que tu cena sea extremadamente aburrida. En cuyo caso, la conversació n de su
2 Capítulo 19. Una mirada rápida al
cita para cenar se convierte en el ruido y la conversació n en la mesa de al lado en la señ al.
Capítulo 19. Una mirada rápida al 2

aprendizaje no supervisado, el problema es má s difícil. Por lo general, elegimos


características en funció n de nuestra intuició n acerca de qué características pueden
ser relevantes para los tipos de estructura que nos gustaría encontrar.

Considere la Figura 19.1, que contiene una tabla de vectores de características y la


etiqueta (reptil o no) con la que se asocia cada vector.

Nombre Huev Escam Venenoso Sangr # Reptil


o- as e fría Piern
tendid as
o
Cobra Verdade Verdade Verdadero Verda 0 Sí
ro ro dero
Serpiente de Verdade Verdade Verdadero Verda 0 Sí
cascabel ro ro dero
Boa constrictor FALSO Verdade FALSO Verda 0 Sí
ro dero
Caimán Verdade Verdade FALSO Verda 4 Sí
ro ro dero
rana venenosa Verdade FALSO Verdadero FALS 4 No
ro O
Salmón Verdade Verdade FALSO Verda 0 No
ro ro dero
Pitón Verdade Verdade FALSO Verda 0 Sí
ro ro dero

Figura 19.1 Nombre, características y etiquetas de animales


variados

Un algoritmo de aprendizaje automá tico supervisado (o un ser humano) al que


se le proporciona solo la informació n sobre las cobras no puede hacer mucho
má s que recordar el hecho de que una cobra es un reptil. Ahora, agreguemos la
informació n sobre las serpientes de cascabel. Podemos comenzar a generalizar y
podríamos inferir la regla de que un animal es un reptil si pone huevos, tiene
escamas, es venenoso, tiene sangre fría y no tiene patas.

Ahora, supongamos que se nos pide que decidamos si una boa constrictor es un
reptil. Podríamos responder "no", porque una boa constrictor no es venenosa ni
pone huevos. Pero esta sería la respuesta incorrecta. Por supuesto, no es
sorprendente que intentar generalizar a partir de dos ejemplos pueda llevarnos
por mal camino. Una vez que incluyamos la boa constrictor en nuestros datos de
entrenamiento, podríamos formular la nueva regla de que un animal es un reptil
si tiene escamas, es de sangre fría y no tiene patas. Al hacerlo, estamos
descartando las características de puesta de huevos y venenosas como
irrelevantes para el problema de clasificació n.

Si usamos la nueva regla para clasificar al caimá n, concluimos incorrectamente que,


dado que tiene patas, no es un reptil. Una vez que incluimos al caimá n en los datos
de entrenamiento, reformulamos la regla para permitir que los reptiles tengan
cuatro patas o ninguna. Cuando miramos a la rana dardo, concluimos correctamente
que no es un reptil, ya que no es de sangre fría. Sin embargo, cuando usamos nuestra
regla actual para clasificar el salmó n, concluimos incorrectamente que un salmó n es
un reptil. Podemos agregar aú n má s complejidad a nuestra regla, para separar el
salmó n de los caimanes, pero es una batalla perdida. No hay forma de modificar
nuestra regla para que clasifique correctamente tanto al salmó n como a las pitones,
ya que los vectores de características de estas dos especies son idé nticos.

Este tipo de problema es má s comú n que no en el aprendizaje automá tico. Es


bastante raro tener vectores de características que contengan suficiente
2 Capítulo 19. Una mirada rápida al
informació n para clasificar las cosas perfectamente. En este caso, el problema es
que no tenemos suficientes funciones. Si nosotros
Capítulo 19. Una mirada rápida al 2

Si hubié semos incluido el hecho de que los huevos de los reptiles tienen
amnios,129 podríamos idear una regla que separe a los reptiles de los peces.
Desafortunadamente, en la mayoría de las aplicaciones prá cticas de aprendizaje
automá tico no es posible construir vectores de características que permitan
una discriminació n perfecta.

¿Significa esto que debemos rendirnos porque todas las funciones disponibles son
mero ruido? No. En este caso los rasgos escamados y la sangre fría son condiciones
necesarias para ser reptil, pero no condiciones suficientes. La regla tiene escamas y
es de sangre fría, no dará falsos negativos, es decir, cualquier animal clasificado
como no reptil no será reptil. Sin embargo, arrojará algunos falsos positivos, es
decir, algunos de los animales clasificados como reptiles no será n reptiles.

19.2 Métricas de distancia


En la figura 19.1, describimos animales usando cuatro características binarias y
una característica de entero. Supongamos que queremos usar estas
características para evaluar la similitud de dos animales, por ejemplo, para
preguntar, ¿es una boa constrictor má s parecida a una serpiente de cascabel o a
una rana dardo?130

El primer paso para hacer este tipo de comparació n es convertir las características
de cada animal en una secuencia de nú meros. Si decimos Verdadero = 1 y Falso = 0,
obtenemos los siguientes vectores de características:
Serpiente de cascabel: [1,1,1,1,0]
Boa constrictora: [0,1,0,1,0]
Rana dardo: [1,0,1,0,4]

Hay muchas maneras diferentes de comparar la similitud de vectores de nú meros.


Las métricas má s utilizadas para comparar vectores de igual longitud se basan en la
distancia de Minkowski:
𝑙𝑒𝑛

𝑑i𝑠𝑡𝑎𝑛𝑐𝑒 𝑉1, 𝑉2, 𝑝 = ( 𝑎𝑏𝑠 𝑉1 −i 𝑉2 𝑝)1 𝑝


i!1

El pará metro p define los tipos de caminos que se pueden seguir al recorrer la
distancia entre los vectores 𝑉1 y 𝑉2. Esto se puede visualizar
fá cilmente en su mayoría si los vectores tienen una longitud
de dos y representan coordenadas cartesianas. Considere la
imagen de la izquierda. ¿El círculo en la esquina inferior
izquierda está má s cerca de la cruz o de la estrella? Eso
depende. Si podemos viajar en línea recta, la cruz está má s
cerca. El Teorema de Pitá goras nos dice que la cruz es la raíz
cuadrada de 8 unidades del círculo, unas 2,8 unidades,
mientras que podemos

129Los amnios son capas externas protectoras que permiten que los huevos se pongan en la
tierra en lugar del agua.
130Estapregunta no es tan tonta como parece. Un naturalista y un toxicó logo (o alguien que
busca mejorar la efectividad de un dardo de soplado) podría dar respuestas diferentes a esta
pregunta.
2 Capítulo 19. Una mirada rápida al

vea fá cilmente que la estrella está a 3 unidades del círculo. Estas distancias se
denominan distancias euclidianas y corresponden al uso de la distancia de
Minkowski con p = 2. Pero imagine que las líneas en la imagen corresponden a
calles y que uno tiene que permanecer en las calles para ir de un lugar a otro. En
ese caso, la estrella permanece a 3 unidades del círculo, pero la cruz ahora está a
4 unidades. Estas distancias se denominan distancias de Manhattan,131 y
corresponden al uso de la distancia de Minkowski con p = 1.

La figura 19.2 contiene una implementació n de la distancia de Minkowski.

Figura 19.2 Distancia de Minkowski


def minkowskiDist(v1, v2, p):
"""Asume que v1 y v2 son matrices de números de igual
La figura 19.3 contiene la clase Animal. Define la distancia entre dos animales
longitud Devuelve la distancia de Minkowski de orden p
como la distancia
entreeuclidiana
v1 y v2"""entre los vectores de características asociados con
los animales.
distancia = 0.0
para i en el rango (len (v1)):
dist += abs(v1[i] - 19.3 ClaseAnimal
Figura
clase Animal(objeto):
def19.4
La figura __init__(yo,
contiene unanombre,
funció ncaracterísticas):
que compara una lista de animales entre sí y
"""Asume el nombre de una cadena; presenta una lista de
produce una tabla que muestra las distancias por pares.
números""" self.name = nombre
self.características = pylab.array(características)
131Laisla de Manhattan es el distrito má s densamente poblado de la ciudad de Nueva York. En
la mayordef getName(self):
parte de la isla, las calles está n dispuestas en cuadrícula, por lo que usar la distancia
de Minkowskireturn
con p = self.nombre
1 proporciona una buena aproximació n de la distancia que uno tiene que
recorrer para caminar desde un lugar (digamos el Museo de Arte Moderno en 53 rdcalle y
defa getFeatures(self):
6elAvenue) otra (por ejemplo, el American Folk Art Museum en 66 elcalle y 9el, tambié n
return
llamada Avenida Coló n). Conducir en Manhattan es una historia totalmente diferente.
self.features

def distancia(yo, otro):


"""Asume otro un animal
Devuelve la distancia euclidiana entre los vectores
de características de self y other"""
devuelve minkowskiDist(self.getFeatures(),
Capítulo 19. Una mirada rápida al 2

def compareAnimals(animales, precisión):


"""Asume que animales es una lista de animales, precision an int
>= 0 Construye una tabla de distancia euclidiana entre cada
animal"""
#Obtener etiquetas para
columnas y filas columnLabels
= []
para a en animales:
columnLabels.append(a.getName())
etiquetas_fila =
etiquetas_columna[:]
valores_tabla = []
#Obtener distancias entre pares de
animales #Para cada fila
para a1 en
animales:
fila = []
#Para cada
columna de a2 en
animales:
si a1 == a2:
fila.append('-
-') más:
distancia = a1.distancia(a2)
fila.append(str(ronda(distancia, precisión)))
tableVals.append(fil
a) #Producir tabla
table = pylab.table(rowLabels = rowLabels,
colLabels =
columnLabels, cellText
= tableVals, cellLoc =
'center',
Figura 19.4 Construir tabla de distancias entre parejas de
animales

El có digo utiliza una funció n de trazado de PyLab que no hemos utilizado anteriormente:
table.

La funció n de tabla produce una grá fica que (¡sorpresa!) parece una tabla. Los
argumentos de palabra clave rowLabels y colLabels se utilizan para proporcionar
las etiquetas (en este ejemplo, los nombres de los animales) para las filas y
columnas. El argumento de palabra clave cellText se utiliza para proporcionar los
valores que aparecen en las celdas de la tabla. En el ejemplo, cellText está vinculado
a tableVals, que es una lista de listas de cadenas. Cada elemento de tableVals es una
lista de los valores de las celdas en una fila de la tabla. El argumento de palabra
clave loc se usa para especificar en qué lugar de cada celda debe aparecer el texto, y
el argumento de palabra clave loc se usa para especificar en qué parte de la figura
debe aparecer la tabla. El ú ltimo pará metro de palabra clave utilizado en el ejemplo
es colWidths. Está vinculado a una lista de flotantes que dan el ancho (en pulgadas)
de cada columna de la tabla. El có digo table.scale(1, 2.
2 Capítulo 19. Una mirada rápida al

Si ejecutamos el có digo
serpiente de cascabel = Animal('serpiente de
cascabel', [1,1,1,1,0]) boa = Animal('boa\
nconstrictor', [0,1,0,1,0]) ranadardo =
Animal('rana dardo' , [1,0,1,0,4]) animales =
[serpiente de cascabel, boa, rana dardo]
compareAnimals(animales, 3)

produce una figura que contiene la tabla

Como probablemente esperabas, la distancia entre la serpiente de cascabel y la


boa constrictor es menor que entre cualquiera de las serpientes y la rana dardo.
Observe, por cierto, que la rana dardo parece estar un poco má s cerca de la
serpiente de cascabel que de la boa.

Ahora, agreguemos al final del có digo anterior las líneas


caimán = Animal('cocodrilo', [1,1,0,1,4])
animales.append(cocodrilo)
compararAnimales(animales, 3)

produce la mesa

Tal vez te sorprenda que el caimá n esté considerablemente má s cerca de la rana


dardo que de la serpiente de cascabel o la boa constrictor. Tó mese un minuto para
pensar por qué .

El vector de características del caimá n difiere del de la serpiente de cascabel en


dos lugares: si es venenoso y el nú mero de patas. El vector de características del
caimá n difiere del de la rana dardo en tres lugares: si es venenoso, si tiene
escamas y si es de sangre fría. Sin embargo, de acuerdo con nuestra mé trica de
distancia, el caimá n se parece má s a la rana dardo que a la serpiente de cascabel.
¿Qué está sucediendo?

La raíz del problema es que las diferentes características tienen diferentes


rangos de valores. Todas menos una de las características oscilan entre 0 y 1,
pero el nú mero de patas oscila entre 0 y 4. Esto significa que cuando calculamos
la distancia euclidiana, el nú mero de patas adquiere un peso desproporcionado.
vamos a ver que
Capítulo 19. Una mirada rápida al 2

sucede si convertimos la característica en una característica binaria, con un valor de


0 si el animal no tiene patas y 1 en caso contrario.

Esto parece mucho má s plausible.

Por supuesto, no siempre es conveniente usar solo funciones binarias. En la secció n


19.7 presentaremos un enfoque má s general para tratar las diferencias de escala
entre las características.

19.3 Agrupación
Agrupaciónse puede definir como el proceso de organizar objetos en grupos
cuyos miembros son similares de alguna manera. Una cuestió n clave es definir el
significado de “similar”.

Considere la grá fica de la derecha, que muestra


la altura, el peso y si llevan o no una camisa a
rayas para 13 personas.

Si queremos agrupar a las personas por altura,


hay dos grupos obvios, delimitados por la línea
horizontal punteada. Si queremos agrupar a las
personas por peso, hay dos grupos obvios
diferentes, delimitados por la línea vertical só lida.
Si queremos agrupar a las personas en funció n de
su camiseta,
todavía hay un tercer agrupamiento, delimitado por las flechas punteadas en á ngulo.
Nó tese, por cierto, que esta ú ltima divisió n no es lineal, es decir, no podemos separar
a las personas que visten camisas a rayas de las demá s mediante una sola línea recta.

La agrupació n en clú steres es un problema de optimizació n. El objetivo es


encontrar un conjunto de clú steres que optimice una funció n objetivo, sujeto a
algú n conjunto de restricciones. Dada una métrica de distancia que se puede usar
para decidir qué tan cerca está n dos ejemplos entre sí, necesitamos definir una
funció n objetivo que

 Minimiza la distancia entre ejemplos en los mismos grupos, es decir,


minimiza la disimilitud de los ejemplos dentro de un grupo.
Como veremos má s adelante, la definició n exacta de la funció n objetivo puede
influir mucho en el resultado.

Una buena medida de qué tan cerca está n los ejemplos dentro de un solo grupo, c,
entre sí es la varianza. Para calcular la varianza de los ejemplos dentro de un
conglomerado,
2 Capítulo 19. Una mirada rápida al

primero calcule la media de los vectores de características de todos los ejemplos en


el grupo. Si V es una lista de vectores de características, cada uno de los cuales es una
matriz de nú meros, la media (má s precisamente, la media euclidiana) es el valor de
la expresió n sum(V)/float(len(V)). Dada la media y una mé trica para calcular la
distancia entre los vectores de características, la varianza de un grupo es

2
𝑣𝑎𝑟$𝑎𝑛𝑐𝑒 𝑐 = 𝑑i𝑠𝑡𝑎𝑛𝑐𝑒(𝑚𝑒𝑎𝑛 𝑐 , 𝑒)2
𝑒∈𝑐

Tenga en cuenta que la varianza no está normalizada por el tamañ o del


conglomerado, por lo que es probable que los conglomerados con má s puntos
parezcan menos cohesivos segú n esta medida. Si uno quiere comparar la coherencia
de dos conglomerados de diferentes tamañ os, necesita dividir la varianza de cada
uno por el tamañ o del conglomerado.

La definició n de varianza dentro de un solo conglomerado, c, puede extenderse para definir


una métrica de disimilitud para un conjunto de conglomerados, C:

𝑑i𝑠𝑠i𝑚i𝑙𝑎𝑟i𝑡𝑦 𝐶 = 𝑣𝑎𝑟$𝑎𝑛𝑐𝑒(𝑐)
𝑐∈𝐶
Observe que dado que no dividimos la varianza por el tamañ o del conglomerado, un
conglomerado incoherente grande aumenta el valor de la disimilitud (C) má s que un
conglomerado incoherente pequeñ o.

Entonces, ¿el problema de optimizació n es encontrar un conjunto de


conglomerados, C, tal que se minimice la disimilitud (C)? No exactamente. Se puede
minimizar fá cilmente colocando cada ejemplo en su propio grupo. Necesitamos
agregar alguna restricció n. Por ejemplo, podríamos imponer una restricció n a la
distancia entre los grupos o exigir que el nú mero má ximo de grupos sea k.

En general, resolver este problema de optimizació n es computacionalmente


prohibitivo para la mayoría de los problemas interesantes. En consecuencia, las
personas confían en algoritmos codiciosos que brindan soluciones aproximadas.
Má s adelante en este capítulo, presentamos uno de estos algoritmos, el
agrupamiento de k-medias. Pero primero presentaremos algunas abstracciones que
son ú tiles para implementar ese algoritmo (y tambié n otros algoritmos de
agrupamiento).
Capítulo 19. Una mirada rápida al 2

19.4 Tipos Ejemplo y Clúster


El ejemplo de clase se utilizará para crear las muestras que se agrupará n. Asociado
con cada ejemplo hay un nombre, un vector de características y una etiqueta
opcional. El mé todo de distancia devuelve la distancia euclidiana entre dos
ejemplos.
ejemplo de clase (objeto):
Figura 19.5 ClaseEjemplo

Class def __init__(self, nombre, funciones, etiqueta =


Cluster, figura 19.6, es un poco má s complejo. Piense en un clú ster como un
Ninguno): #Supone que las funciones son una
conjunto de ejemplos. Los dos mé
matriz de números todos interesantes
self.name = nombre en Cluster son
computeCentroid y variance.
self.features = Piense en el centroide de un cú mulo como su centro de
características
masa. El método computeCentroid devuelve un ejemplo con un vector de
self.label = etiqueta
características igual a la media euclidiana de los vectores de características de los
ejemplos
def en el clú ster. La varianza del mé todo proporciona una medida de la
dimensionalidad(self):
return
coherencia del grupo.
len(self.features)

def getFeatures(self):
return
self.features[:]

def getLabel(self):
return
self.label

def getName(self):
return
self.nombre
2 Capítulo 19. Una mirada rápida al

clase Clúster (objeto):

def __init__(self, ejemplos, tipo de ejemplo):


"""Asume que los ejemplos son una lista de ejemplos de tipo
tipoejemplo""" self.ejemplos = ejemplos
self.tipoEjemplo = tipoEjemplo
self.centroid =
self.computeCentroid()

actualización def (auto, ejemplos):


"""Reemplazar los ejemplos en el clúster por nuevos
ejemplos. Devuelve cuánto ha cambiado el
centroide"""
oldCentroid =
self.centroid
self.examples = ejemplos
si len(ejemplos) > 0:
self.centroid = self.computeCentroid()
return oldCentroid.distance(self.centroid)
demás:
volver 0.0

miembros def (uno mismo):


for e in
self.examples:
yield e

tamaño def (auto):


return len(auto.ejemplos)

def getCentroid(self):
return
self.centroid

def computarCentroide(auto):
dim =
self.ejemplos[0].dimensionalidad()
totVals = pylab.array([0.0]*dim)
para e en self.examples:
totVals +=
e.getFeatures()
centroide = self.ejemploTipo('centroide',
totVals/float(len(self.examples)))
centroide de retorno

def
varianza(auto
): totDist =
0.0
para e en self.ejemplos:
totDist += (e.distance(self.centroid))**2
return totDist**0.5

def
__str__(self
Figura 19.6 ClaseGrupo
Capítulo 19. Una mirada rápida al 2

19.5 Clúster de K-medias


Agrupamiento de K-mediases probablemente el mé todo de agrupamiento má s
utilizado.132 Su objetivo es dividir un conjunto de ejemplos en k grupos tales que

1. Cada ejemplo está en el grupo cuyo centroide es el centroide má s


cercano a ese ejemplo, y
2. La disimilitud del conjunto de conglomerados se minimiza.
Desafortunadamente, encontrar una solució n ó ptima a este problema en un gran
conjunto de datos es computacionalmente intratable. Afortunadamente, existe un
algoritmo voraz eficiente133 que se puede utilizar para encontrar una
aproximació n ú til. Está descrito por el pseudocó digo.
elegir aleatoriamente k ejemplos como centroides
iniciales mientras sea verdadero:
1) crear k grupos asignando cada ejemplo al centroide más cercano
2) calcule k nuevos centroides promediando los ejemplos en cada grupo
3) si ninguno de los centroides difiere de la iteración
anterior: devolver el conjunto actual de clústeres

La complejidad del paso 1 es O(k*n*d), donde k es el nú mero de grupos, n es el


nú mero de ejemplos y d el tiempo necesario para calcular la distancia entre un par
de ejemplos. La complejidad del paso 2 es O(n), y la complejidad del paso 3 es O(k).
Por lo tanto, la complejidad de una sola iteració n es O(k*n*d). Si los ejemplos se
comparan utilizando la distancia de Minkowski, d es lineal en la longitud del vector
de características.134 Por supuesto, la complejidad de todo el algoritmo depende del
nú mero de iteraciones. Que no es fá cil de caracterizar, pero baste decir que suele ser
pequeñ o.

Un problema con el algoritmo de k-medias es que no es determinista: el valor


devuelto depende del conjunto inicial de centroides elegidos al azar. Si se elige un
conjunto particularmente desafortunado de centroides iniciales, el algoritmo podría
establecerse en un ó ptimo local que está lejos del ó ptimo global. En la prá ctica, este
problema generalmente se aborda ejecutando k-means varias veces con centroides
iniciales elegidos al azar. Luego elegimos la solució n con la mínima disimilitud de
conglomerados.

La figura 19.7 contiene una traducció n directa del pseudocó digo que describe k-
means a Python. Utiliza random.sample(examples, k) para obtener los centroides
iniciales. Esta invocació n devuelve una lista de k elementos distintos elegidos
aleatoriamente de la lista de ejemplos.

132Aunque el agrupamiento de k-medias es probablemente el mé todo de agrupamiento


má s utilizado, no es el método má s apropiado en todas las situaciones. Otros dos mé todos
ampliamente utilizados, que no se tratan en este libro, son el agrupamiento jerá rquico y el
agrupamiento EM.
133Elalgoritmo de k-means má s utilizado se atribuye a James McQueen y se publicó por
primera vez en 1967. Sin embargo, ya en la dé cada de 1950 se utilizaron otros enfoques
para la agrupació n de k-means.
134Desafortunadamente, en muchas aplicaciones necesitamos usar una métrica de
distancia, por ejemplo, la distancia de los movimientos de tierra o la distancia de
deformació n diná mica del tiempo, que tienen una mayor complejidad computacional.
2 Capítulo 19. Una mirada rápida al

def kmeans(ejemplos, tipoejemplo, k, detallado):


"""Asume que los ejemplos son una lista de ejemplos de tipo
tipoEjemplo, k es un int positivo, verbose es un valor
booleano
Devuelve una lista que contiene k clústeres. Si verbose
es True, imprime el resultado de cada iteración de k-
means"""
#Obtener k centroides iniciales elegidos
aleatoriamente initialCentroids =
random.sample(examples, k)

#Crear un clúster singleton para cada clúster


de centroide = []
para e en initialCentroids:
clusters.append(Cluster([e], exampleType))

#Iterar hasta que los centroides no


cambien convergentes = Falso
numIterations = 0
mientras no
converge:
numeroIteraciones += 1
#Crear una lista que contenga k listas vacías distintas
newClusters = []
para i en el rango (k):
newClusters.append
([])

#Asocie cada ejemplo con el centroide más


cercano para e en los ejemplos:
#Encontrar el centroide más cercano a e
la distancia más pequeña =
e.distance(clusters[0].getCentroid()) índice = 0
para i en el rango (1, k):
distancia =
e.distancia(clústeres[i].getCentroid()) si
distancia <distancia más pequeña:
distancia más pequeña =
índice de distancia = i
#Agregue e a la lista de ejemplos para el clúster apropiado
newClusters[index].append(e)

#Actualizar cada clúster; verificar si un centroide ha


cambiado convergente = Verdadero
para i en el rango (len (clusters)):
si clusters[i].update(newClusters[i]) > 0.0:
convergente = False
si es detallado:
imprime 'Iteración #' + str(numIteraciones)
para c en clústeres:
imprimir c
imprimir '' #agregar
agrupaciones de retorno de línea
en blanco

Figura 19.7 Agrupamiento de K-medias

La figura 19.8 contiene una funció n, trykmeans, que llama a kmeans varias veces y
selecciona el resultado con la disimilitud má s baja.
Capítulo 19. Una mirada rápida al 2

def disimilitud(clusters):
totDist = 0.0
para c en grupos:
totDist +=
c.varianza() return
totDist

def trykmeans(ejemplos, tipoejemplo, numClusters, numTrials,


verbose = False):
"""Llama a kmsignifica numTrials veces y devuelve el resultado con
la diferencia más baja"""
mejor = kmeans(ejemplos, tipoejemplo, numClusters, detallado)
minDisimilitud = disimilitud(mejor)
para prueba en rango (1, numTrials):
clusters = kmeans(ejemplos, tipoejemplo, numClusters,
detallado) currDisimilitud = disimilitud(clusters)
si currDissimilarity
<minDissimilarity: mejor =
clústeres

Figura 19.8 Encontrar el mejor agrupamiento de k-medias

19.6 Un ejemplo artificial


La figura 19.9 contiene có digo que genera, traza y agrupa ejemplos extraídos de
dos distribuciones.

La funció n genDistributions genera una lista de n ejemplos con vectores de


características bidimensionales. Los valores de los elementos de estos vectores de
características se extraen de distribuciones normales.

La funció n plotSamples traza los vectores de características de un conjunto de ejemplos.


Utiliza otra característica de trazado de PyLab que aú n no hemos visto: la funció n anotar
se usa para colocar texto junto a puntos en el grá fico. El primer argumento es el texto, el
segundo argumento el punto con el que está asociado el texto y el tercer argumento la
ubicació n del texto en relació n con el punto con el que está asociado.

La funció nPrueba artificialusosgenDistribucionespara crear dos distribuciones


de diez ejemplos, cada una con la misma desviació n está ndar pero diferentes medias,
grafica los ejemplos usandoplotSamplesy luego los agrupa usandoTryksignifica.
2 Capítulo 19. Una mirada rápida al

def genDistribution(xMean, xSD, yMean, ySD, n, namePrefix):


muestras = []
para s en el rango (n):
x = random.gauss(xMedia,
xSD) y =
random.gauss(yMedia, ySD)
samples.append(Ejemplo(namePrefix+str(s), [x, y]))
muestras de retorno

def plotSamples(muestras,
marcador): xVals, yVals =
[], []
para s en muestras:
x = s.getFeatures()
[0] y =
s.getFeatures()[1]
pylab.annotate(s.getName(), xy = (x, y),
xytext = (x+0.13, y-
0.07), tamaño de fuente
= 'x-grande')
xVals.append(
x)
yVals.append(
y)
pylab.plot(xVals, yVals, marcador)

def contrivedTest(numTrials, k, detallado):


random.seed(0)
x Media = 3
xSD = 1
yMedia = 5
ySD = 1
norte = 10
d1Muestras = genDistribution(xMedia, xSD, yMedia, ySD, n, '1.')
plotSamples(d1Muestras, 'b^')
d2Muestras = genDistribution(xMedia+3, xSD, yMedia+1, ySD, n, '2.')
plotSamples(d2Muestras, 'ro')
Figura 19.9 Una prueba de k-medias

Cuando se ejecuta, la llamadaprueba artificial (1, 2, verdadero)produjo el


grá fico de la figura 19.10.

Figura 19.10 Ejemplos de dos distribuciones


Capítulo 19. Una mirada rápida al 2

e impreso
Iteración 1
El clúster con centroide [ 4.57800047 5.35921276] contiene:
1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 2.0, 2.1, 2.2, 2.3,
2.4, 2.5, 2.6, 2.7, 2.8, 2.9
El clúster con centroide [ 3.79646584 2.99635148]
contiene: 1.9

Iteración 2
El clúster con centroide [4.80105783 5.73986393] contiene:
1.1, 1.2, 1.4, 1.5, 1.6, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7,
2.8, 2.9
El clúster con centroide [ 3.75252146 3.74468698]
contiene: 1.0, 1.3, 1.7, 1.8, 1.9

Iteración 3
Clúster con centroide [5.63888356.02296994] contiene:
1.6, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9
El clúster con centroide [ 3.19452848 4.28541384]
contiene: 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.7, 1.8, 1.9

Iteración 4
El clúster con centroide [ 5.93613865 5.96069975]
contiene: 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8,
2.9
El clúster con centroide [ 3.14170883 4.52143963]
contiene: 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8,
1.9

Iteración 5
El clúster con centroide [ 5.93613865 5.96069975]
contiene: 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8,
2.9
El clúster con centroide [ 3.14170883 4.52143963]
contiene: 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8,
1.9

Resultado final
El clúster con centroide [ 5.93613865 5.96069975]
contiene: 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8,
2.9
El clúster con centroide [ 3.14170883 4.52143963]
contiene: 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8,
1.9

Observe que los centroides iniciales (elegidos al azar) dieron lugar a un


agrupamiento muy sesgado en el que un solo grupo contenía todos los puntos
menos uno. Sin embargo, en la quinta iteració n, los centroides se habían movido a
lugares tales que los puntos de las dos distribuciones se separaron limpiamente en
dos grupos. Dado que se puede usar una línea recta para separar los puntos
generados a partir de la primera distribució n de los generados por la segunda
distribució n, no es terriblemente sorprendente que las k-medias convergieran en
este agrupamiento.

cuando intentamos40ensayos en lugar de1,llamandoprueba artificial (40, 2,


falso), imprimió

Resultado final
El clúster con centroide [ 6.07470389 5.67876712]
contiene: 1.8, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7,
2.8, 2.9
El clúster con centroide [ 3.00314359 4.80337227]
contiene: 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.9

Esto indica que la solució n encontrada usando 1 ensayo, a pesar de separar


2 Capítulo 19. Una mirada rápida al
perfectamente los ejemplos por la distribució n de la que fueron elegidos, no fue tan
buena
Capítulo 19. Una mirada rápida al 2

(con respecto a minimizar la funció n objetivo) como una de las soluciones


encontradas usando 40 ensayos.

Ejercicio de dedos:Dibuje líneas en la figura 19.10 para mostrar las separaciones


encontradas por nuestros dos intentos de agrupar los puntos. ¿Está de acuerdo en
que la solució n encontrada con 40 ensayos es mejor que la encontrada con 1
ensayo?

Uno de los problemas clave en el uso


del agrupamiento de k-medias es elegir
k. Considere los puntos en la grá fica de
la derecha, que se generaron usando
contrivedTest2, Figura 19.11. Esta
funció n genera y agrupa puntos a partir
de tres distribuciones gaussianas
superpuestas.

Figura 19.11 Generación de puntos a partir de tres distribuciones


def contrivedTest2(numTrials, k, detallado):
random.seed(0)
la invocació nprueba2(40, 2, Falso)huellas dactilares
x Media = 3
xSD = 1final
Resultado
El yMedia
clúster= con
5 centroide [ 7.66239972 3.55222681]
ySD = 1 2.0, 2.1, 2.3, 2.6
contiene:
El norte = 8con centroide [ 3.36736761 6.35376823] contiene:
clúster
d1Muestras
1.0, = genDistribution(xMedia,xSD,
1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 2.2, yMedia, ySD, 2.7,
2.4, 2.5, n, '1.')
3.0,
3.1,plotSamples(d1Muestras,
3.2, 3.3, 3.4, 3.5, 3.6,'b^')
3.7
d2Muestras = genDistribution(xMedia+3,xSD,yMedia, ySD, n, '2.')
plotSamples(d2Muestras, 'ro')
d3Muestras = genDistribution(xMedia, xSD, yMedia+3, ySD, n, '3.')
plotSamples(d3Muestras, 'gd')
clusters = trykmeans(d1Muestras + d2Muestras + d3Muestras,
Ejemplo, k, numTrials, detallado)
imprime 'Resultado
final' para c en
grupos:
2 Capítulo 19. Una mirada rápida al

la invocació nprueba2(40, 3, Falso)huellas dactilares


Resultado final
El clúster con centroide [ 7.66239972 3.55222681]
contiene: 2.0, 2.1, 2.3, 2.6
El clúster con centroide [ 3.10687385 8.46084886]
contiene: 3.0, 3.1, 3.2, 3.4, 3.5, 3.6, 3.7
El clúster con centroide [ 3.50763348 5.21918636] contiene:
1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 2.2, 2.4, 2.5, 2.7, 3.3

Y la invocació nprueba2(40, 6, Falso)huellas dactilares


Resultado final
El clúster con centroide [ 7.66239972 3.55222681]
contiene: 2.0, 2.1, 2.3, 2.6
El clúster con centroide [ 2.80974427 9.60386549]
contiene: 3.0, 3.6, 3.7
El clúster con centroide [ 3.70472053 4.04178035]
contiene: 1.0, 1.3, 1.5
El clúster con centroide [ 2.10900238 4.99452866]
contiene: 1.1, 1.2, 1.4, 1.7
El clúster con centroide [4.92742554 5.60609442] contiene:
2.2, 2.4, 2.5, 2.7
El clúster con centroide [ 3.27637435 7.28932247]
contiene: 1.6, 3.1, 3.2, 3.3, 3.4, 3.5

El ú ltimo agrupamiento es el que mejor se ajusta, es decir, el agrupamiento tiene


la disimilitud má s baja. ¿Significa esto que es el “mejor” ajuste? Recuerde que
cuando observamos la regresió n lineal en la Secció n 15.1.1, observamos que al
aumentar el grado del polinomio obtuvimos un modelo má s complejo que
proporcionó un ajuste má s ajustado a los datos. Tambié n observamos que
cuando aumentamos el grado del polinomio, corríamos el riesgo de encontrar un
modelo con un valor predictivo bajo, porque se ajustaba demasiado a los datos.

Elegir el valor correcto para k es exactamente aná logo a elegir el polinomio de


grado correcto para una regresió n lineal. Al aumentar k, podemos disminuir la
disimilitud, a riesgo de sobreajuste. (Cuando k es igual al nú mero de ejemplos que
se van a agrupar, ¡la diferencia es cero!) Si tenemos alguna informació n sobre có mo
se generaron los ejemplos que se van a agrupar, por ejemplo, elegidos de m
distribuciones, podemos usar esa informació n para elegir k . En ausencia de tal
informació n, hay una variedad de procedimientos heurísticos para elegir k.
Adentrarse en ellos está má s allá del alcance de este libro.

19.7 Un ejemplo menos artificial


Diferentes especies de mamíferos tienen diferentes há bitos alimenticios.
Algunas especies (p. ej., elefantes y castores) solo comen plantas, otras (p. ej.,
leones y tigres) solo comen carne y algunas (p. ej., cerdos y humanos) comen
todo lo que se les mete en la boca. Las especies vegetarianas se denominan
herbívoras, las carnívoras se denominan carnívoras y las especies que comen
ambas se denominan omnívoras.

Durante milenios, la evolució n (o algú n otro proceso misterioso) ha dotado a las


especies de dientes adecuados para el consumo de sus alimentos preferidos. Eso
plantea la cuestió n de si la agrupació n de mamíferos en funció n de su dentició n
produce grupos que tienen alguna relació n con sus dietas.
Capítulo 19. Una mirada rápida al 2

La tabla de la derecha
muestra el contenido de un #Nombre
archivo que enumera #incisivos
algunas especies de superiores
#caninos
mamíferos, sus fó rmulas
superiores
dentales (los primeros 8 #premolares
nú meros), su peso adulto superiores
promedio en libras,135 y #molares
superiores
un có digo que indica su #incisivos
dieta preferida. Los inferiores
comentarios en la parte #caninos
inferiores
superior describen los
#premolares
elementos asociados con inferiores
cada mamífero, por #molares
ejemplo, el primer inferiores
#peso
elemento que sigue al
#Etiqueta: 0=herbívoro, 1=carnívoro,
nombre es el nú mero de 2=omnívoro Tejón,3,1,3,1,3,1,3,2,10,1
incisivos superiores. Oso,3,1,4,2,3,1,4,3,278,2
Castor,1,0,2,3,1,0,1,3,20,0
La figura 19.12 contiene Murciélago marrón,2,1,1,3,3,1,2,3,0.5,1
una funció n, Gato,3,1,3,1,3,1,2,1,4,1
Puma,3,1,3,1,3,1,2,1,63,1
readMammalData, para leer
Vaca,0,0,3,3,3,1,2,1,400,0
un archivo formateado de Ciervo,0,0,3,3,4,0,3,3,200,0
esta manera y procesar el Perro,3,1,4,2,3,1,4,3,20,1
contenido del archivo para Zorro,3,1,4,2,3,1,4,3,5,1
Lobo marino,3,1,4,1,2,1,4,1,200,1
producir un conjunto de Foca gris,3,1,3,2,2,1,3,2,268,1
ejemplos que representen Conejillo de indias,1,0,1,3,1,0,1,3,1,0
la informació n del archivo. Alce,0,1,3,3,3,1,3,3,500,0
Primero procesa la Humano,2,1,2,3,2,1,2,3,150,2
Jaguar,3,1,3,1,3,1,2,1,81,1
informació n del encabezado Canguro,3,1,2,4,1,0,2,4,55,0
al comienzo del archivo León,3,1,3,1,3,1,2,1,175,1
para obtener un recuento Visón,3,1,3,1,3,1,3,2,1,1
Mol,3,1,4,3,3,1,4,3,0,75,1
de la cantidad de
Alce,0,0,3,3,4,0,3,3,900,0
características que se Ratón,1,0,0,3,1,0,0,3,0.3,2
asociará n con cada ejemplo. Puercoespín,1,0,1,3,1,0,1,3,3,0
Luego usa las líneas Cerdo,3,1,4,3,3,1,4,3,50,2
Conejo,2,0,3,3,1,0,2,3,1,0
correspondientes a cada
especie para construir tres
listas:

 especiesNombreses
una lista de los
nombres de los
mamíferos.

 etiquetaListaes una lista de las etiquetas asociadas con los mamíferos.

 valor de característicaes una lista de listas. cada elemento devalor de


característicacontiene la lista de valores, uno para cada mamífero, para una
sola característica. El valor de la expresió nvalores de característica [i]
[j]es elielcaracterística de lajelmamífero.
2 Capítulo 19. Una mirada rápida al

135Incluimos la informació n sobre el peso porque al autor le han dicho en má s de una ocasió n
que existe una relació n entre su peso y sus há bitos alimenticios.
Capítulo 19. Una mirada rápida al 2

La ú ltima parte de readMammalData usa los valores en featureVals para crear una lista
de vectores de características, uno para cada mamífero. (El có digo podría simplificarse
al no construir valores de características y, en cambio, construir directamente los
vectores de características para cada mamífero. Elegimos no hacer eso en previsió n de
una mejora para readMammalData que haremos má s adelante en esta secció n).

def readMammalData(fName):
dataFile = open(fName, 'r')
numFeatures = 0
#Líneas de proceso en la parte superior del archivo
para línea en archivo de datos: #Buscar número de características
if line[0:6] == '#Label': #indica el final de la
interrupción de funciones
if linea[0:5] !=
'#Nombre':
numFeatures += 1
valores de característica = []

#Producir valores de características, nombres de


especies y lista de etiquetas valores de
características, nombres de especies, lista de
etiquetas = [], [], [] para i en rango (número de
características):
featureVals.append([])

#Continúe procesando las líneas en el archivo, comenzando


después de los comentarios de la línea en el archivo de
datos:
dataLine = string.split(line[:-1], ',') #remove newline; luego divide
especiesNombres.append(dataLine[0])
classLabel = float(dataLine[-1])
labelList.append(classLabel) for
i in range(numFeatures):
featureVals[i].append(float(dataLine[i+1]))

#Usar featureVals para crear una lista que contenga los


vectores de características #para cada mamífero
listaVectorCaracterísticas = []
para mamíferos en rango (len (nombres de
especies)): vector de características = []
para característica en rango (numFeatures): featureVector.append
(featureVals[feature][mammal])
featureVectorList.append(featureVector)
return featureVectorList, labelList,
especiesNombres

Figura 19.12 Leer y procesar archivo

La funció n testTeeth en la figura 19.13 usa trykmeans para agrupar los ejemplos
construidos por la otra funció n, buildMammalExamples, en la figura 19.13. Luego
informa la cantidad de herbívoros, carnívoros y omnívoros en cada grupo.
2 Capítulo 19. Una mirada rápida al

def buildMammalExamples(featureList, labelList, especiesNombres):


ejemplos = []
para i en rango (len (nombres de
especies)): características =
pylab.array (lista de
características [i])
ejemplo = Ejemplo(nombresdeespecies[i], características,
listadeetiquetas[i]) ejemplos.append(ejemplo)
devolver ejemplos

def testTeeth(numClusters, numTrials):


características, etiquetas, especies =
readMammalData('dentalFormulas.txt') ejemplos =
buildMammalExamples(características, etiquetas, especies)
bestClustering =\
trykmeans(examples, Example, numClusters, numTrials)
para c en bestClustering:
nombres = ''
para p en c.miembros():
nombres += p.getName() + ', '
print '\n', nombres[:-2] #eliminar coma final y espacio
herbívoros, carnívoros, omnívoros = 0, 0, 0
para p en c.miembros():
if p.getLabel() ==
0: herbívoros
+= 1
elif p.getLabel() ==
1: carnívoros +=
1
Figura 19.13 Agrupamiento de animales

Cuando ejecutamos el có digotestTeeth(3, 20)imprimió


Vaca, alce, alce, león marino
3 herbívoros, 1 carnívoros, 0 omnívoros

Tejón, puma, perro, zorro, conejillo de indias, jaguar, canguro,


visón, topo, ratón, puercoespín, cerdo, conejo, mapache, rata,
murciélago rojo, mofeta, ardilla, marmota, lobo
4 herbívoros, 9 carnívoros, 7 omnívoros

Oso, Ciervo, Lobo marino, Foca gris, Humano, León


1 herbívoros, 3 carnívoros, 2 omnívoros

Hasta aquí nuestra conjetura de que el agrupamiento estaría relacionado con los
há bitos alimenticios de las diversas especies. Una inspecció n superficial sugiere que
tenemos un agrupamiento totalmente dominado por los pesos de los animales. El
problema es que el rango de pesos es mucho mayor que el rango de cualquiera de las
otras características. Por lo tanto, cuando se calcula la distancia euclidiana entre
ejemplos, la ú nica característica que realmente importa es el peso.

Encontramos un problema similar en la Secció n 19.2 cuando descubrimos que la


distancia entre los animales estaba dominada por el nú mero de patas. Allí
resolvimos el problema convirtiendo el nú mero de patas en una característica
binaria (con patas o sin patas). Eso estuvo bien para ese conjunto de datos, porque
todos los animales tenían cero o cuatro patas. Aquí, sin embargo, no hay forma de
binarizar el peso sin perder una gran cantidad de informació n.
Capítulo 19. Una mirada rápida al 2

Este es un problema comú n, que a menudo se soluciona escalando las


características para que cada característica tenga una media de 0 y una desviació n
está ndar de 1, como lo hace la funció n scaleFeatures en la figura 19.14.

Figura 19.14 Atributos de escala


def scaleFeatures(valores):
"""Asume
Para ver el efecto que vals veamos
de scaleFeatures, es una elsecuencia
siguiente códe
digo.
números""" result = pylab.array(vals)
v1, v2 = [],media[] =
para i en suma(resultado)/float(largo(resultado))
rango (1000): v1.append
resultado(100,
(random.gauss = resultado
5)) - media
sd =
v2.append(random.gauss(50, 10))
stdDev(resultado)
v1 = scaleFeatures(v1)
resultado = de escala (v2)
v2 = características
imprime 'v1 significa =', round(suma(v1)/len(v1), 4),\
'desviación estándar v1', round(stdDev(v1), 4)
print 'v2 mean =', round(sum(v2)/len(v2), 4),\
'desviación estándar v1', round(stdDev(v2), 4)

El có digo genera dos distribuciones normales con diferentes medias (100 y 50) y
diferentes desviaciones está ndar (5 y 10). Luego escala cada uno e imprime las
medias y las desviaciones está ndar de los resultados. Cuando se ejecuta, imprime
v1 media = -0,0 v1 desviación estándar
1,0 v2 media = 0,0 v1 desviación estándar
1,0136

Es fá cil ver por qué la declaració n result = result - mean asegura que la media de la
matriz devuelta siempre estará cerca de 0137. Que la desviació n está ndar siempre sea
1 no es obvio. Se puede demostrar mediante una larga y tediosa cadena de
manipulaciones algebraicas, con las que no te aburriremos.

La figura 19.15 contiene una versió n de readMammalData que permite escalar


características. La nueva versió n de la funció n testTeeth en la misma figura muestra
el resultado de la agrupació n con y sin escala.

136Una distribució n normal con una media de 0 y una desviació n está ndar de 1 se llama
distribución normal estándar.
137Decimos “cerrar”, porque los nú meros de punto flotante son solo una aproximació n a los
reales y el resultado no siempre será exactamente 0.
2 Capítulo 19. Una mirada rápida al

def readMammalData(fName, escala):


"""Asume que la escala es un valor booleano. Si es Verdadero,

las funciones se escalan""" #el inicio del código es el mismo

que en la versión anterior

#Usar featureVals para crear una lista que contenga los


vectores de características #para cada característica a escala
de mamífero, si es necesario
si escala:
para i en el rango (numFeatures):
featureVals[i] = scaleFeatures(featureVals[i]) #el

resto del código es el mismo que en la versión anterior

def testTeeth(numClusters, numTrials, scale):


características, etiquetas, especies =\
readMammalData('dentalFormulas.txt', scale)
ejemplos = buildMammalExamples(características, etiquetas,
Figura 19.15 Código que permite escalar características

Cuando ejecutamos el có digo


imprimir 'Cluster sin escalar'
testTeeth(3, 20, False)
print '\nCluster with scaling'
testTeeth(3, 20, True)

se imprime
Clúster sin escalar

Vaca, alce, alce, león marino


3 herbívoros, 1 carnívoros, 0 omnívoros

Tejón, puma, perro, zorro, conejillo de indias, jaguar, canguro,


visón, topo, ratón, puercoespín, cerdo, conejo, mapache, rata,
murciélago rojo, mofeta, ardilla, marmota, lobo
4 herbívoros, 9 carnívoros, 7 omnívoros

Oso, Ciervo, Lobo marino, Foca gris, Humano, León


1 herbívoros, 3 carnívoros, 2 omnívoros

Racimo con escamas

Vaca, ciervo, alce, alce


4 herbívoros, 0 carnívoros, 0 omnívoros

Conejillo de indias, canguro, ratón, puercoespín, conejo, rata,


ardilla, marmota
4 herbívoros, 0 carnívoros, 4 omnívoros

Tejón, Oso, Puma, Perro, Zorro, Lobo marino, Foca gris, Humano,
Jaguar, León, Visón, Topo, Cerdo, Mapache, Murciélago rojo, León
marino, Mofeta, Lobo
0 herbívoros, 13 carnívoros, 5 omnívoros
Capítulo 19. Una mirada rápida al 2

El agrupamiento con escala no divide perfectamente a los animales en funció n de


sus há bitos alimenticios, pero ciertamente se correlaciona con lo que comen los
animales. Hace un buen trabajo al separar a los carnívoros de los herbívoros, pero
no hay un patró n obvio en el lugar donde aparecen los omnívoros. Esto sugiere que
quizá s se necesiten otras características ademá s de la dentició n y el peso para
separar a los omnívoros de los herbívoros y carnívoros.138

19.8 Terminando
En este capítulo, apenas hemos arañ ado la superficie del aprendizaje automá tico.
Hemos tratado de darle una idea del tipo de pensamiento involucrado en el uso del
aprendizaje automá tico, con la esperanza de que encuentre formas de abordar el
tema por su cuenta.

Lo mismo podría decirse de muchos de los otros temas presentados en este libro.
Hemos cubierto mucho má s terreno de lo que es típico en los cursos introductorios
de ciencias de la computació n. Probablemente encontraste algunos temas menos
interesantes que otros. Pero esperamos que haya encontrado al menos algunos
temas sobre los que desea aprender má s.

138La posició n de los ojos puede ser una característica ú til, ya que tanto los omnívoros como
los carnívoros suelen tener los ojos en la parte delantera de la cabeza, mientras que los ojos
de los herbívoros suelen estar má s hacia los lados. Entre los mamíferos, solo las madres de los
humanos tienen ojos en la parte posterior de la cabeza.
2 Capítulo 19. Una mirada rápida al

PYTHON 2.7 REFERENCIA RÁPIDA


Operaciones comunes en tipos numéricos

yo+jes la suma deiyj.


yo-jesimenosj.
i*jes el producto de iyj.
yo//jes divisió n entera.
yo/jes i dividido por j. En Python 2.7, cuando i y j son del tipo int, el resultado
tambié n es un int; de lo contrario, el resultado es un float.
yo% jes el resto cuando elent ise divide por elintj.
i**jesielevado a the poderj.
x + = yes equivalente ax = x + y.*=y-=trabajar de la misma manera.
Comparación y operadores booleanos

x == ydevolucionesVerdaderosiXyyson iguales.
x != ydevolucionesVerdaderosiXyyno son iguales
<, >, <=, >=tienen sus significados habituales.
a y BesVerdaderosi ambosaybsonVerdadero, yFALSOde lo contrario.
a o BesVerdaderosi al menos uno deaobesVerdadero, yFALSOde lo contrario.
No unesVerdaderosiaesFALSO, yFALSOsiaesVerdadero.
Operaciones comunes en tipos de secuencia

secuencia[i]devuelve la ielelemento en la
secuencia.len(siguiente)devuelve la longitud
de la secuencia.sec1 + sec2concatena las dos
secuencias.
n * seqdevuelve una secuencia que se repite seq n veces.
seq[inicio:fin]devuelve un segmento de la secuencia.
e en secuenciaprueba simiestá contenido en la secuencia.
e no en secuenciaprueba simino está contenido en la secuencia.
para e en seqitera sobre los elementos de la secuencia.
Métodos de cadena comunes

s.count(s1)cuenta cuá ntas veces ocurre la cadena s1 en s.


para encontrar (s1)devuelve el índice de la primera aparició n de la subcadenas1ens;-1
sis1no está dentros.
s.rfind(s1)igual queencontrar, pero comienza desde el final
des.s.índice(s1)igual queencontrar, pero plantea una excepció n
sis1no está dentros.s.rindex(s1)igual queíndice, pero comienza
desde el final des.Más lento()convierte todas las letras mayú sculas a
minú sculas.
s.replace(antiguo, nuevo)reemplaza todas las apariciones de cadena viejocon
cuerdanuevo.
s.rstrip()elimina el espacio en blanco final.
s.split(d)divisionessusandodcomo delimitador. Devuelve una lista de subcadenas de s.
288 Referencia rápida de Python

Métodos de lista comunes


L.añadir(e)agrega el objeto e al final de L.
L.cuenta(e)devuelve el nú mero de veces que aparece e enL.
L.insertar(i, e)inserta el objetomienLen el
índicei.L.extender(L1)agrega los elementos en la listaL1al final
deL.L.quitar(e)elimina la primera aparició n demideL.índice
L(e)devuelve el índice de la primera aparició n demienL.
L pop (i)elimina y devuelve el artículo en el índice i. El valor predeterminado es -1.
Ordenar L()tiene el efecto secundario de ordenar los elementos de L.
L.reversa()tiene el efecto secundario de invertir el orden de los elementos en L.
Operaciones comunes en los diccionarios
prestar)devuelve el nú mero de elementos en d.
d.teclas()devuelve una lista que contiene las claves
end.d.valores()devuelve una lista que contiene los
valores end.amabledevolucionesVerdaderosi clavekes
end.
d[k]devuelve el artículo endcon llavek. aumentaError de clavesikno está dentrod.
d.obtener(k, v)devuelve d[k] si k en d, y v en caso contrario.
d[k] = vasocia el valor v con la clave k. Si ya hay un valor asociado con k, ese
valor se reemplaza.
del d[k]quita elemento con llavekded. aumentaError de clavesikno está dentrod.
para k en ditera sobre las claves end.
Comparación de tipos no escalares comunes

Tipo de
Tipo Tipo de elemento Ejemplos de literales Mudable
índice

calle En t caracteres '','a','a B C' No

tupla En t cualquier tipo (), (3,), ('abc', 4) No

lista En t cualquier tipo [], [3], ['abc', 4] Sí

dicta hashable cualquier tipo {}, {'a':1}, Sí


r objetos {'a':1, 'b':2.0}

Mecanismos comunes de entrada/salida


raw_input(mensaje)imprime msg y luego devuelve el valor ingresado como
una cadena.imprimir s1,…, snimprime cadenas s1,…, sn con un espacio
entre cada uno.abierto ('nombre de archivo', 'w')crea un archivo para
escribir.
open('nombreArchivo', 'r')abre un archivo existente para
lectura.open('nombreArchivo', 'a')abre un archivo existente para
agregarlo.manejararchivo.read()devuelve una cadena que contiene el
contenido del archivo.manejararchivo.readline()devuelve la siguiente
línea en el archivo.manejararchivo.readlines()devuelve una lista que
contiene líneas del archivo.fileHandle.write(s)escriba la cadena s al
final del archivo.fileHandle.writelines(L)Escribe cada elemento de L en
el archivo.manejararchivo.cerrar()cierra el archivo.
ÍNDICE
funció n binaria, 270
ini , 94 nú mero binario, 122, 154
cial peroilt-‐ bú squeda binaria, 128
iza inmé todo,98
r
nombre peroilt-‐ técnica de depuració n de bú squeda binaria, 80
inmé todo,183
calle , 95 á rbol binario, 254
unió n, de nombres,
11
abdominalessperoilt-‐in funció n,20 Biblia, 200
tipo de datos abstractos. Véase notació n O grande. Ver complejidad
abstracció n de abstracció n de datos, 43 computacional
barrera de abstracció n, 91, 140
aceleració n de la gravedad, 208
algoritmo, 2
alias, 61, 66
prueba para, 73
al-‐Khwarizmi,mahomad Ibnmusa, 2
Museo de Arte Popular Estadounidense,
267 anotació n, trazado PyLab, 276
Anscombe, FJ, 226
agregar método, 61
soluciones aproximadas, 25
una funció n de
rango, 218 arco de
grá fico, 240
Arquímedes, 201
argumentos, 35
tipo de matriz, 148
operadores, 216
afirmar declaració n,
90
afirmaciones, 90
declaració n de asignació n, 11
mú ltiplo, 13, 57
mutació n versus, 58
desempaquetar mú ltiples valores
devueltos, 57

Babbage, Charles, 222


Bachiller, Luis, 179
retrocediendo, 246, 247
grá fico de barras, 224
béisbol, 174
Botones, Richard, 252
ley de Benford, 173
Bernoulli, Jacob, 156
teorema de Bernoulli, 156
bú squeda de bisecció n, 27, 28
poco, 29
trama de aspecto extrañ o, 145
bcaja de carenciaspruebaEn g.Semi
pruebaEn g,bcaja de carenciasbloques de
có digo, 15
Boesky, Ivá n, 240
expresió n booleana, 11
compuesto, 15
cortocircuito
tEvaluació n,49Box, George
EP, 205 programas de
bifurcació n, 14 bú squeda
en amplitud (BFS), 249
declaració n de
interrupció n, 23
Marró n, Rita
Mae, 79
Marró n,
Robert, 179
movimiento browniano, 179
Buffon, 201
bicho, 76
encubierto, 77
intermitente,
77 origen de
la palabra, 76
manifiesto,
77
persistent
e, 77
funciones
integradas
abdominales, 20
ayuda, 41
identificació n, 60
entrada, 18
es una instancia, 101
len, 17
lista, 63
mapa, 65
má ximo, 35
minuto, 57
gama, 23
entrada_cruda, 18
redondo, 31
clasificado, 131, 136, 236
suma, 110
2 Índ

tipo, 10 clases de complejidad, 118, 123–24


rango x, 24, 197 computació n, 2
byte, 1 complejidad computacional, 16, 113–24
análisis amortizado, 131
C++, 91 notació n asintó tica, 116
Coordenadas cartesianas, 180, promedio--caso,114
266 mejor--caso,114
caso--sensibilidad,12 notació n O grande, 117
no determinismo causal, 152 Notació n Big Theta,
centroide, 272 118 constante, 16, 118
nodo hijo, 240 esperado--caso,114
Iglesia, Alonso, 36 exponencial, 118, 121
Iglesia-‐Turingeles, 3Toboganes y inherentemente exponencial, 239
escaleras, 191 clase variable, lineal, 118, 119
95, 99 logarítmico, 118
clases, 91–112
en método, 94 log-‐lineal,118,120
eson método, 183 límite inferior, 118
ombr
e
calle método, 95 polinomio, 118, 120
resumen, 109 pseudo polinomio, 260
atributo, 94 cuadrá tico, 120
referencia de atributo, 93 reglas generales para expresar,
variable de clase, 95, 99 117 límite estricto, 118
atributo de datos, 94, 95 tIME-espaciocomerciosi,140,199
definició n, 94 límite superior, 114, 117
definició n, 92 el peor--
notació n de puntos, 94 caso,114concatenació
herencia, 99 n (+)
ejemplo, 94 añ adir, vs., 62
variable de instancia, 95 listas, 62
instanciació n, 93, 94 tipos de secuencia, 16
funció n isinstance, 101 tuplas, 56
isinstance vs. type, 102 complejidad conceptual, 113
atributo de método, 93 conjunto, 48
atributos primordiales, 99 Doctrina de Copenhague, 152
instancias de impresió n, 95 copiar mó dulo de biblioteca
uno mismo, 94 está ndar, 63 correlació n, 225
subclase, 99 dados, 195
superclase, 99 validació n cruzada, 221
jerarquía de tipos, 99
tipo frente a abstracció n de datos, 92, 95–96, 179
isinstance, 102 mó dulo de biblioteca está ndar de
cliente, 42, 105 fecha y hora, 96 depuració n, 41, 53,
método close para 70, 76–83, 90
archivos, 53 CLU, 91 programas estocásticos, 157
agrupamiento, 270 nú meros decimales, 29
coeficiente de variació n, 163, á rbol de decisiones, 254–56
165 comando. Ver comentario descomposició n, 43
de sentencia en programas, 12 funció n decreciente, 21, 130
compilador, 7 funció n de copia profunda,
Índ 2
63 valores
de
pará metro
s
predetermi
nados, 37
2 Índ

programació n defensiva, 77, 88, 90


Euclides, 172
fó rmula dental, 281
distancia euclidiana, 267
profundidad--
Media euclidiana, 271
primerobuscarh(DFS),246 nodo de
Euler, Leonhard, 241
destino, 240
excepto bloque, 85
programa determinista, 153
excepciones, 84–
tipo de dictado, 67–69
90 incorporado
agregar un elemento,
Error de aserció n, 90
69 claves permitidas,
ÍndiceError, 84
69 eliminar un
Error de nombre, 84
elemento, 69 claves, 67
Error de tipo, 84
método de llaves, 67, 69
Error de valor, 84
método de valores, 69
peroilt-‐inCmuchacha,87
diccionario. Ver tipo de
manejo, 84–87
dictado Dijkstra, Edsger, 70
crianza, 84
dimensionalidad, de datos,
probar–excepto, 85
264 disjunció n, 48
sin manejar, 84
dispersió n, 165
algoritmos de enumeració n exhaustiva,
métrica de disimilitud, 271
21, 22, 26, 234, 254
distribuciones, 160
algoritmo de raíz cuadrada, 26, 116
curva de campana. Ver
decaimiento exponencial, 172
distribuciones, Benford normal, 173
crecimiento exponencial, 172
regla empírica para normal, 169
expresió n, 9
Gaussiana. Ver distribuciones,
extender método,
propiedad normal sin memoria,
62 extender una
171
lista, 62
normales, 169, 168–70, 202
uniforme, 137, 170
factoriales, 45, 115, 120
ddivide y vencerá saalgoritmos,132,261
implementació n iterativa, 45, 115
divide y
implementació n recursiva, 45
vencerá sproblemalemaentoncesviviendo,
falso negativo, 266
49cadena de documentos, 41
falso positivo, 266
línea de no pase,
extracció n de características, 264
notació n de 195
vector de características, 263
puntos, 48, 52, 94
poema de Fibonacci, 47
Dra. Pangloss, 70
Secuencia de Fibonacci, 45,
programació n diná mica, 252–61
252–54 programació n
dinamismoic-‐time-warping,274
diná mica
implementació n, 253
tierra-‐
implementació n recursiva, 46
mudanzasddistancia,274borde de
sistema de archivos, 53
un grá fico, 240 programas
archivos, 53–55, 54
eficientes, 125
agregando, 54
Einstein, Albert, 70, 179
método cerrado, 53
límite elá stico de resortes,
mango de archivo, 53
213 elif, 15
funció n abierta, 53
má s, 14, 15
lectura, 54
encapsulació n, 105
método de escritura, 53
ENIAC, 193
barras de error, 169
personaje de escape, 53
Índ 2
vaso--cajapruebaEn g.Semi pruebaEn g,gramolass-‐
escribir, 53 cajaó ptimo global, 240
primera declaració n mundial, 51
claseVirginialues,64,86ajuste
de una curva a los datos, 210–
14
coeficiente de determinació n (R2),
216 exponencial con polyfit, 218
mínimos cuadradostransmisió n
exterioryectivo funció n,210regresió n
lineal, 211
funció n objetivo, 210
sobreajuste, 213
polifit, 211
programa fijoordenadores,
2 tipo flotador. Ver coma
flotante coma flotante, 9, 30,
29–31
exponente, 30
precisió n, 30
reales vs., 29
valor redondeado, 30
errores de redondeo, 31
dígitos significativos, 30
disquete, 142
flujo de control,
3 bucle for, 54
para generadores
de extractos,
107
Franklin, Benjamín, 50
funció n, 35
pará metro real, 35
argumentos, 35
como objeto, 64–65
como pará metro, 135
llamar, 35
clase como pará metro, 183
valores de pará metro
predeterminados, 37
definició n, 35
invocació n, 35
argumento de palabra clave, 36,
37 enlace de pará metro
posicional, 36

falacia del jugador, 157


Distribució n gaussiana. Ver
distribuciones, normal
generalizació n, 262
generador, 107
distribució n geométrica, 172
progresió n geométrica, 172
2 Índ
ayudaperoilt-‐in funció n,41
variable global, 50, 75 funciones auxiliares, 48, 129
grá fico, 240–51 Garza de Alejandría, 1
representació n de lista de horden superior funciones,sesenta y cinco
adyacencia, 243 horden superiorprogramaEn g,64
representació n de matriz de histograma, 166
adyacencia, 243 amplitud Hoare, COCHE, 135
primerobuscarh(BFS),249 conjunto de reserva, 221, 232
profundidad-- Holmes, Sherlock, 82
primerobuscarh(DFS),246 Ley de Hooke, 207, 213
dígrafo, 240 Hopper, Grace Murray, 76
grá fico dirigido, 240 terapia de reemplazo hormonal,
borde, 240 226 precios de la vivienda, 223
teoría de grafos, 241 Huff, Darrell, 222
n
o identificació nperoilt-‐
d in
o funció n,60INACTIVO,
, 13
2
4
0
p
r
o
b
l
e
m
a
s
camarillas, 244
corte mínimo, 244, 246
ruta má s corta,
244, 246–51 ruta
ponderada má s
corta, 244
ponderado, 241
Graunt, Juan, 222
gravedad, aceleració n
debido a, 208 algoritmo
codicioso, 235
adivinar-‐y-‐verificaraalgoritmos,2,22

problema de detenció n, 3
aldea, 77
simulació n de mano, 19
hash, 69, 137–40
colisió n, 137, 138
cubos de hachís, 138
funció n hash, 137
tablas hash, 137
probabilidad de
colisiones, 177
Índ 2

editar menú , 13
conocimiento, declarativo versus
menú archivo, 13
imperativo, 1 Knuth, Donald, 117
si declaració n, 15
Problema de los puentes de Kö nigsberg, 241
tipo inmutable, 58
declaració n de importació n, 52
etiqueta argumento de
en operador, 66 sangría
palabra clave, 146
de có digo, 15 eventos
abstracció n lambda, 36
independientes, 154
Lampson, mayordomo, 128
indexació n para tipos de
Regazocordó n,PAGierre-‐Simon,201
secuencia, 17 indirecció n, 127
ley de los grandes numeros,
inducció n, 132
156, 157 hoja, de arbol, 254
definició n inductiva, 45
ajuste de mínimos
estadística inferencial, 155
cuadrados, 210, 212 len
ocultació n de informació n, 105, 106
funció n incorporada, 17
entrada, 18
longitud, para tipos de
aporteperoilt-‐in
secuencia, 17 Leonardo de
funció n,18raw_input frente a, 18
Pisa, 46
instancia, de una clase, 93
alcance léxico, 38
entorno de desarrollo integrado
biblioteca, Python está ndar,
(IDE), 13
consulte también mó dulos de
interfaz, 91
biblioteca está ndar, 53
intérprete, 3, 7
regresió n lineal, 211, 262
Introducción a los Algoritmos,
Liskov, Bá rbara, lista
125 es instanciaperoilt-‐in
103peroilt-‐in
funció n,101 iteració n, 18
funció n,63 lista de
para bucle, 23
comprensió n, 63
sobre enteros, 23
tipo de lista, 58–62
sobre listas, 61 + (concatenación)
operador, 62 clonación,
Java, 91 63
Julieta, 12 comprensió n, 63
Julio César, 50 copiar, 63
indexació n, 126
Kennedy, José, 81 representació n interna, 127
clave, en una parcela. Ver trazado literales, 4, 288
en PyLab, funció n de leyenda ó ptimo local, 240
argumento de palabra clave, 36 variable local, 38
palabras clave, 12 funció n logarítmica,
k--medioClustre,274–86 220 logaritmo, base
problema de la mochila, 234–40 de, 118 eje
0/1, 238 logarítmico, 124
bruto--fuerzaentonceslució n,238 escala logarítmica, 159
solució n de programació n diná mica, bucle, 18
254– 61 bucle invariante, 131
fraccionario (o continuo), 240 operador lt, 133
Knight Capital Group, 78 variable al acecho, 225

có digo má quina,
7 aprendizaje
automático
supervisado, 263
2 Índ
sin supervisió n, 264
distancia manhattan, 267
Proyecto Manhattan, 193
Índ 2

muchos--a unomapaEn g,137


mapagperoilt-‐in O notació n. Ver complejidad
funció n,65MATLAB, 141 computacional O(1). Ver complejidad
mamá Xperoilt-‐in computacional,
funció n,35 constante
memorizació n, 253 Obama, Barack, 44
propiedad sin memoria, 171 objeto, 9–11
invocació n del método, clase, 99
48, 94 minperoilt-‐in primera clase,64
funció n,57 mudable, 58
distancia de Minkowski, 266, 269, 274 igualdad de objetos, 60
mó dulos, 51–53, 51, 74, 91 igualdad de valores
Moksha-‐patamú,191 vs., 81
Moliere, 92 funció n objetivo, 210, 263, 270
Simulació n Monte Carlo, 193– transmisió n exteriororientado a
204 Monty Python, 13 objetosprogramaEn g,91funció n
hipotecas, 108, 146 abierta para archivos, 53
mulínea ltideclaraciones,22 precedencia de operadores, 10
asignació n mú ltiple, 12, 13, 57 mó dulo de biblioteca está ndar de
devolver valores de funciones, operadores, 133 operadores, 9
58 -‐,onortearreglos,148
tipo mutable, 58 -‐,onortenú meros,10
mutació n versus asignació n, 58 *, en arreglos, 148
*, en nú meros, 10
espacio de nombres, 37 *, sobre secuencias, 66
nombres, 12 **, en nú meros, 10
nan (no es un nú mero), *=, 25
88 nanosegundos, 22 /, en nú meros, 10
Asociació n Nacional del Rifle, //, en nú meros, 10
229 nú mero natural, 45 %, en nú meros, 10
declaraciones anidadas, 15 +, en nú meros, 10
cará cter de nueva línea, 53 +, en secuencias, 66
newton'smé todo.Semi newton- +=, 25
Raphsonmétodo -‐=,25
mecánica newtoniana, 152 booleano, 11
newton-Raphsonmé todo,32,33,126, punto flotante, 10
210 en, en secuencias,
Nixon, Richard, 56 66 infijo, 4
nodo de un grá fico, entero, 10
240 no en, en secuencias, 66
no determinismo, causal frente a sobrecarga, 16
predictivo, 152 solució n ó ptima, 238
Ninguno, 9, 110 subestructura ó ptima, 252, 258
no--escalartipo,56 problema de optimizació n, 210, 234, 263, 270
distribució n normal. Ver restricciones, 234
distribuciones, normal funció n objetivo, 234
estándar, xiii, 284 orden de crecimiento,
no en operador, 66 117 sobreajuste, 213,
hipó tesis nula, 174, 231 280
operadores numéricos, 10 subproblemas superpuestos, 252,
tipos numéricos, 9 258 sobrecarga de operadores, 16
numpy, 148
2 Índ

palíndromo, 48
coeficiente, 32
má quina paralela de acceso
grado, 32
aleatorio, 114 nodo principal, 240
ajuste polinomial, 211
Pascual, Blaise, 194
método pop, 62
línea de pase, 195
hacer estallar una
declaració n de aprobació n, 101
pila, 39
caminos a través de la
formato de grá ficos de red portá tiles,
especificació n, 72 Peters, Tim,
142 power set, 122, 238
136
no determinismo predictivo, 152
pi (π), estimació n por simulació n, 200–
declaració n de impresió n, 18
204 Pingala, 47
probabilidades, 154
Pirandello, 43
programa, 8
trazado en PyLab, 141–46, 166–68, 190
lenguaje de programació n, 3, 7
anotar, 276
compilado, 7
grá fico de barras, 224
hde alto nivel, 7
cifra actual, 143
interpretado, 7
configuració n predeterminada, 146
nivel bajo, 7
funció n figura, 141
semá ntica, 5
cadena de formato, 144
semá ntica está tica, 4
histograma, 166
sintaxis, 4
argumentos de palabra clave,
aviso, concha, 10
145 argumento de palabra
experimento prospectivo, 221
clave de etiqueta, 146
estudio prospectivo, 232
etiquetas para diagramas,
PyLab, véase también
146
trazado, 141
funció n de leyenda, 146
una funció n de rango, 218
marcadores, 189
matriz, 148
funció n grá fica, 141
polifit, 211
configuració n rc, 145
guía del usuario, 141
funció n savefig, 143
teorema de Pitá goras, 180, 202
funció n semilogx, 159
Pitó n, 7, 35
funció n de semilogía, 159
Python 3, versus 2.7, 8, 9, 18, 24
mostrar funció n, 142
Declaració n de Python, 8
estilo, 187
mesas, 268
mecá nica cuá ntica, 152
funció n de título, 144
ventanas, 141
conejos, 46
funció n xlabel, 144
elevar declaració n, 87
garrapatas, 224
má quina de acceso aleatorio,
funció n ylabel, 144
114 mó dulo aleatorio, 153,
garrapatas, 224
172
extensió n de archivo png,
elecció n, 153
142 puntos de ejecució n,
gauss, 170
36 puntos, en tipografía,
al azar, 153
145 puntero, 127
muestra, 274
polifit, 210
semilla, 157
ajuste de una funció n
uniforme, 170
polimó rfica, 218
paseo aleatorio, 179–92
exponencial, 86
parcial, 186
polinomio, 32
Índ 2

sonó miperoilt-‐in concha, 8


funció n,23Python 2 contra 3, indicador de shell, 10
24 cortocircuito
crudo_aporteperoilt-‐in tEvaluacionoFAbucheoinclinarseex
funció n,18entrada contra, 18 presiones, 49
recurrencia, 46 efecto secundario, 61, 62
recursividad, 44 sseñ al a ruidoratayo,264
caso base, 44 dígitos
caso recursivo (inductivo), 44 significativos, 30
prueba de regresió n, 76 simulació n
regresió n a la media, 157 lanzamiento de moneda, 155–65
declaració n de recarga, 53 determinista, 205
eliminar método, 62 Montecarlo, 193–204
representació n invariante, 95 juicios mú ltiples, 156
representanteindependencia de paseos aleatorios, 179–92
iones,95palabras reservadas en prueba de humo, 184
Python, 12 estudio retrospectivo, 232 estocá stico, 205
retorno de la inversió n (ROI), estructura tipica, 196
196 declaració n de retorno, 35 modelo de simulació n, 155, 205
método inverso, 62 continuo, 206
pará metro inverso, 236 discreto, 206
Papiro Rhind, 200 diná mico, 206
raíz, 254 está tica, 206
raíz de polinomio, 32 resumen de, 204–6
funció n integrada redonda, segmentació n, para tipos de
31 R-cuadrado, 216 secuencia, 17 SmallTalk, 91
prueba de humo, 184
funció n de muestra, Serpientes y escaleras,
274 muestreo 191 SNR, 264
exactitud, 159 redes sociales, 246
sesgo, 228 aseguramiento de la calidad del
confianza, 160, 162 software, 75 ordenar método
Samuel, Arturo, 262 incorporado, 98, 131
tipo escalar, 9 método de clasificació n, 62, 136
características de escala, 284 pará metro clave, 136
alcance, 37 pará metro inverso, 136
léxico, 38 clasificadodperoilt-‐in funció n,131,136,236
estática, 38 algoritmos de clasificació n, 131–37
guion, 8 en su lugar,134
algoritmos de bú squeda, 126–30 ordenar por fusió n, 120, 134, 252
bú squeda binaria, 128, 129 clasificació n rá pida, 135
bú squeda de bisecció n, 28 ordenació n estable, 137
amplitud--primerobuscarh(BFS),249 Timsort, 136
primero en có digo fuente, 7
profundidadbuscarh(DFS),246 nodo fuente, 240
bú squeda lineal, 114, 126 complejidad espacial, 120, 135
espacio de bú squeda, 126 especificació n, 41–44
uno mismo, 94 supuestos, 42, 129
semá ntica, 5 cadena de documentos, 41
tipos de secuencia, 17, Ver str, tupla, garantías, 42
lista funció n de divisió n para cadenas, 135
3 Índ

constante de resorte, 207


extrapolació n, 229
SQA, 75
Garbage In Garbage Out (GIGO), 222
raíz cuadrada, 25, 26, 27, 32
ignorando el contexto, 229
ordenació n estable, 137
no--respuestabías,228 confianza en las
pila, 39
medidas, 226 Falacia del
marco de pila, 38
francotirador de Texas, 230
desviació n estándar, 160, 169,
conclusió n estadísticamente
198 relativa a la media, 163
vá lida, 204 estadísticas
copia de mó dulos de
coeficiente de variació n, 165 intervalo
biblioteca está ndar, 63
de confianza, 165, 168, 169
fecha y hora, 96
nivel de confianza, 168
matemá ticas, 220
correcció n vs., 204
operador, 133
correlació n, 225
al azar, 153
barras de error, 169
cuerda, 135
hipó tesis nula, 174
distribució n normal estándar,
pag--valor,174
declaració n 284, 8
prueba para, 174
declaracion
paso (de un cá lculo), 114
es
proceso estocástico, 153
afirman,
almacenado--
90
programacomputadora, 3calle
asignació n (=), 11
* operador, 16
descanso, 23, 24 + operador, 16
condicional, 14
peroilt-‐inmé todos,66
para bucle, 23, 54
concatenació n (+), 16
mundial, 51
cará cter de escape, 53, 100
si, 15 indexació n, 17
importar, 52
len, 17
importar *, 52
cará cter de nueva línea, 53
pasar, 101
rebanar, 17
declaració n de impresió n, 18
subcadena, 17
subir, 87 estraLínea rectaprogramas,14
recargar, 53
mó dulo de biblioteca está ndar de
volver, 35
cadena, tipo de cadena 135. Ver
probar–excepto, 85
calle
mientras bucle, 19
talones, 75
rendimiento, 107
principio de sustitució n, 103, 244
alcance estático, 38 subcadena, 17
verificació n semá ntica está tica, 5, 106 aproximació n sucesiva, 32, 210
semá ntica está tica, 4 sumaperoilt-‐in funció n,110
aprendizaje automá tico aprendizaje supervisado, 263
estadístico, 262 pecado tabla de símbolos, 38, 52
estadístico, 222–33 sintaxis, 4
asumiendo independencia, 223
confundiendo correlació n y bú squeda de tablas, 199–
causalidad, 200, 253 tablas, en PyLab,
225 268 terminació n
muestreo de conveniencia (accidental),
228 Cum Hoc Ergo Propter Hoc, 225
engañ ar con imá genes, 223
298 subido por [stormrg]
Índ Índice 3

de bucle, 19, 21 método de instancia, 92


de recursividad, 130 entero, 9
pruebas, 70–76 lista. Ver tipo
bcaja de carencias,71,73 de lista
condiciones de contorno, 72 Ninguno, 9
vaso--caja,71,73–74 calle Ver
pruebas de integració n, 74 tupla str,
entradas de partició n, 71 56
camino--completo,73 tipo, 92
pruebas de regresió n, 76
funciones de prueba, 41 tu.S.Cciudadano,DelawarefinitooFnaturalezana
conjunto de pruebas, 71 cí,44Ulam, Estanislao, 193
pruebas unitarias, 74 funció n unaria, 65
Falacia del francotirador de distribució n uniforme. Ver
Texas, 230 pedidos en total, 27 distribuciones, uniforme
datos de entrenamiento, 262 aprendizaje no supervisado, 264
set de entrenamiento, 221, 232
traducir texto, 68 valor, 9
á rbol, 254 igualdad de valores frente a
á rbol de decisiones, 254–56 igualdad de objetos, 81 variable, 11
nodo hoja, 254 elegir un nombre,
primero a la izquierdaprofundidad-- 12 varianza, 160, 271
primeroenumerarion,256raíz, de á rbol, 254 versiones, 8
á rbol binario vértice de un grá fico,
enraizado, 254 bloque de 240 von Neumann, John,
prueba, 85 133 von Rossum, Guido,
intentar--exceptodeclaració n,85 8
tupla, 56–58
Completitud de Turing, 4 mientras bucle, 19
Má quina de Turing, caracteres de espacio en blanco, 135
universal, 3 Ala, Jeannette, 103
turing-‐completoprogramaEn g idioma,34 tamañ o de palabra, 127
tipo, 9, 91 Serie Mundial, 174
elenco, 18 funciones de envoltorio,
conversió n, 18, 147 129 método de escritura
tipoperoilt-‐in para archivos, 53
funció n,10 verificació n
de tipos, 17 Xrangmiperoilt-‐in funció n,24,197
tipo tipo, 92 garrapatas, 224
tipos
bool, 9 declaració n de rendimiento, 107
dictar Ver garrapatas, 224
dictado tipo
flotante, 9 cero--basado indexació n,17

También podría gustarte