Revisión del libro “Python 3, Curso Práctico” de Alberto Cuevas

Nos han pedido una revisión de un nuevo libro sobre Python en español. El libro se titula 'Python 3, Curso Práctico' y lo ha escrito Alberto Cuevas Álvarez. Si seguís leyendo podréis ver cómo ganar una copia en papel del mismo ;-D

Primero de todo, algunas características del libro:

  • Título: Python 3, Curso Práctico.
  • Autor: Alberto Cuevas Álvarez
  • Año de edición: 2016
  • Nº páginas: 560
  • ISBN: 978-84-9964-658-9
  • Editorial: RA-MA EDITORIAL
  • Encuadernación: Rústica
  • Idioma: Español (de España).
  • Versión de Python usada en el libro: 3.3
  • Link: http://www.ra-ma.es/libros/PYTHON-3-CURSO-PRACTICO/94627/978-84-9964-658-9
  • Precio: 31.90 € para la versión en papel.
  • Versión electrónica: No disponible de momento.

Le hemos pedido al autor que nos defina su intención al escribir el libro y esta es la frase resumen que nos ha enviado:

"Mi intención a la hora de realizar el libro ha sido explicar los fundamentos básicos de Python y las herramientas necesarias dentro de su ecosistema para conseguir crear aplicaciones gráficas completas en 2D ."

Como venimos haciendo, vamos a ir viendo el libro capítulo a capítulo para poder comentar de forma pormenorizada las pequeñas piezas que componen el mismo:

INTRODUCCIÓN

El libro empieza fuerte explicando, aunque sea muy por encima, cosas básicas de programación que pueden venir bien para fijar ciertos conceptos si no se dispone de cierto bagaje en ciencias de la computación.

EMPEZANDO A PROGRAMAR

En este punto se introducen conceptos importantes como lo que son las variables, cómo se asignan, ciertas funciones integradas (builtin) en el intérprete, operadores,etc. Cosas básicas introducidas en el momento oportuno. Está bien lo detallado de algún punto con múltiples ejemplos útiles. Se comenta la instalación de PyScripter, un IDE solo windows :-(

ELEMENTOS FUNDAMENTALES DE PROGRAMACIÓN: INSTRUCCIÓN CONDICIONAL Y BUCLES

El capítulo habla de forma extensa del control de flujo, bucles y condiciones. Muy detallada la explicación de las condiciones. En este mismo capítulo se introducen algunas cosas que no tienen mucha relación como el mecanismo del import (explicado muy brevemente), el uso de números aleatorios (que no tiene mucha relación con el resto de contenidos del capítulo), depuración con PyScripter (menos útil para personas fuera de Windows),...

PROGRAMACIÓN FUNCIONAL

El título del capítulo es algo desafortunado ya que no estamos hablando sobre programación funcional sino sobre el uso de funciones en Python y esto podría confundir a alguien. El capítulo es muy completo para aprender a usar funciones en Python, el 'scope' de las variables, los parámetros y argumentos,... Nuevamente, algún ejemplo podría resultar confuso pero, en general, está bastante completo. Echo en falta que se nombre a las funciones lambda.

PROGRAMACIÓN ORIENTADA A OBJETOS

Llegamos a la parte de clases y la programación orientada a objetos (POO). El capítulo es bastante extenso y se explican muchas cosas, algunas de ellas avanzadas. Sin duda, este es el capítulo del libro que me gusta menos. Los ejemplos que se usan no son muy ortodoxos, nuevamente se recurre a casos muy particulares que provocan, siempre en mi modesta opinión, que algunas cosas resulten en más complejas de lo que deberían en este punto. Una buena parte del capítulo se habla sobre como se hacen algunas cosas con PyScripter lo que le resta valor a usuarios fuera de Windows.

TIPOS DE DATOS EN PYTHON

Este capítulo es muy detallado. Se explica con mucha profundidad los tipos básicos en Python, cadenas, listas, tuplas, conjuntos y diccionarios. Podría, incluso, servir como guía de referencia en español del uso de estos tipos  por lo detallado (son unas 90 páginas). Además, se usan muchísimos ejemplos para explicar los conceptos.

FICHEROS Y EXCEPCIONES

Se habla sobre cómo poder usar ficheros para leer y escribir información. Esta parte es muy detallada y extensa con múltiples ejemplos útiles. La parte de excepciones es más que correcta con un alcance adecuado para introducirlos. Por último, se muestra el uso de with pero se despacha en menos de una página por lo que me parece insuficiente. Este capítulo y el anterior son los que más me gustan del libro.

PROGRAMACIÓN GRÁFICA EN PYTHON MEDIANTE PYQT

No es normal encontrar una introducción a interfaces gráficas en un libro introductorio. Como mucho, se muestra un ejemplo básico para dejar al lector que se introduzca por su cuenta en el tópico si tiene interés. En este caso se hace una introducción mucho más extensa que el ejemplo típico por lo que si tienes interés en crear interfaces gráficas estás de suerte. Sin embargo, si no tienes mucho interés en ello, se van muchas páginas en ello. Se explican de forma detallada muchos de los widgets disponibles en PyQt y está muy enfocado a crear las interfaces usando Qt Designer.

GENERACIÓN DE GRÁFICOS EN PYTHON MEDIANTE MATPLOTLIB

Nuevamente, se introduce otra librería que quizá no sea de interés para todo el mundo. En alrededor de 50 páginas se habla sobre cómo instalar la librería, cómo usar pyplot con muchos ejemplos, como usar matplotlib usando POO y cómo integrar matplotlib en una aplicación PyQt. Reitero, si tienes interés en hacer gráficas tienes suerte pero si no es así se te van otras 50 páginas de libro en ello.

Apuntes varios sobre el libro:

Me gusta:

  • Lo detallado de la explicación del if en el capítulo 3.
  • Que no haya referencias a Python 2.
  • Lo detallado de la explicación de las funciones.
  • Lo detallado de la explicación de los tipos básicos de Python pudiendo servir, incluso, como guía de referencia en español.
  • La parte del tratamiendo de ficheros y excepciones se hace con un alto nivel de detalle.
  • La extensión del libro, que permite poder desarrollar algunos temas de forma muy completa.
  • Que sea en español.

No me gusta:

  • El uso de eval en muchos ejemplos del libro lo considero una mala práctica.
  • No se respeta el PEP8, enseñando malas prácticas, lo cual no me parece adecuado en un curso introductorio.
  • Algunos ejemplos usan casos extremos para explicar ciertos conceptos. Estos casos extremos pueden ser detalles de la implementación y no los considero adecuados ya que en lugar de ayudar pueden resultar más confusos.
  • Está muy enfocado a Windows (uso de PyScripter, instalaciones) haciendo que una buena parte de las páginas sea menos útil para gente que use otros sistemas operativos.

Conclusión

El libro tiene algunos altibajos con partes que brillan con luz propia y partes que mejoraría.

Si de mi dependiera me gustaría que algunas cosas se explicasen un poco mejor, como el uso de with, las funciones lambda, el mecanismo del import, la parte de POO. Metería partes con muchas librerías útiles de la librería estándar (math, itertools, datetime, os, sys, collections,...), como crear paquetes y módulos,... Reduciría o eliminaría los dos últimos capítulos.

Como comentario general, considero que el libro está bien pero, como todo en esta vida, se podría mejorar en algunos aspectos.

Sorteamos una copia entre nuestros lectores

El autor nos envió varias copias del libro. Una de ellas la vamos a sortear entre todos los lectores de Pybonacci residentes en España. Para participar:

  • Solo tienes que escribir un tweet indicando porqué te gustaría tener este libro incluyendo un enlace a http://www.ra-ma.es/libros/PYTHON-3-CURSO-PRACTICO/94627/978-84-9964-658-9
  • Una vez enviado el tweet nos lo enlazas en los comentarios de más abajo para que no se nos escape el tweet.
  • Si no tienes cuenta en twitter, déjanos un comentario más abajo indicando porqué te gustaría tener este libro.

Tenéis hasta el miércoles, 2 de noviembre a las 21:00:00 (CET) para participar en el sorteo. Después de pasada la fecha indicaremos cómo se hará el sorteo (actualizando algunas cosas de aquí), usando el número ganador del sorteo de la ONCE de ese día (2016/11/02) y anunciaremos el ganador.

Actualización: Resultado del sorteo

El número de la ONCE fue el 69907. Si introducís el código aquí:

sale que el ganador ha sido @FlixUjo. He usado los participantes por orden de fecha en su comentario, del más antiguo al más nuevo:

participantes = ['FlixUjo', 'Javier @runjaj', 'Antonio Molina',
'Raúl', 'José Carlos Juanos', 'Christian',
'Kike', 'Eduardo Campillos']

Enhorabuena al vencedor, por favor, mándanos un DM por twitter o usa el formulario de contacto para mandarnos una dirección de correo/teléfono o lo que prefieras.

Saludos a todos.

Cómo crear extensiones en C para Python usando CFFI y numba

Introducción

En este artículo vamos a ver cómo crear extensiones en C para Python usando CFFI y aceleradas con numba. El proyecto CFFI ("C Foreign Function Interface") pretende ofrecer una manera de llamar a bibliotecas escritas en C desde Python de una manera simple, mientras que numba, como podéis leer en nuestro blog, es un compilador JIT para código Python numérico. Mientras que hay algo de literatura sobre cómo usar CFFI, muy poco se ha escrito sobre cómo usar funciones CFFI desde numba, una característica que estaba desde las primeras versiones pero que no se completó hasta hace cuatro meses. Puede parecer contradictorio mezclar estos dos proyectos pero en seguida veremos la justificación y por qué hacerlo puede abrir nuevos caminos para escribir código Python extremadamente eficiente.

Este trabajo ha surgido a raíz de mis intentos de utilizar funciones hipergeométricas escritas en C desde funciones aceleradas con numba para el artículo que estoy escribiendo sobre poliastro. El resultado, si bien no es 100 % satisfactorio aún, es bastante bueno y ha sido relativamente fácil de conseguir, teniendo en cuenta que partía sin saber nada de C ni CFFI hace tres días.

¿Por qué CFFI + numba?

Como decíamos CFFI y numba, aunque tienen que ver con hacer nuestros programas más rápidos, tienen objetivos bastante diferentes:

  • CFFI nos permite usar C desde Python. De este modo, si encontramos algún algoritmo que merece la pena ser optimizado, lo podríamos escribir en C y llamarlo gracias a CFFI.
  • numba nos permite acelerar código Python numérico. Si encontramos algún algoritmo que merece la pena ser optimizado, adecentamos un poco la función correspondiente y un decorador la compilará a LLVM al vuelo.

Continue reading

Cómo acelerar tu código Python con numba

Introducción

En este artículo vamos a hacer un repaso exhaustivo de cómo acelerar sustancialmente tu código Python usando numba. Ya hablamos sobre la primera versión de numba en el blog, allá por 2012, pero ha habido importantes cambios desde entonces y la herramienta ha cambiado muchísimo. Recientemente Continuum publicó numba 0.17 con una nueva documentación mucho más fácil de seguir, pero aun así no siempre queda claro cuál es el camino para hacer que funcione, como quedó patente con el artículo sobre Cython de Kiko. Por ello, en este artículo voy a aclarar qué puede y qué no puede hacer numba, cómo sacarle partido y voy a detallar un par de ejemplos exitosos que he producido en los últimos meses.

Continue reading

Ecuaciones de Lotka-Volterra: modelo presa depredador

Introducción

Resulta intuitivo pensar que las poblaciones de un animal depredador y su presa están relacionadas de algún modo en el que si una aumenta, la otra lo hace también. Utilizaremos como ejemplo en este artículo un ecosistema aislado y formado por leones y cebras que viven en armonía, es decir, los leones comiéndose a las cebras. Imaginemos que por cualquier circunstancia, por ejemplo, por disponer de mayor cantidad de alimento, aumenta la población de cebras; los leones dispondrán de más alimento y su población aumentará, pero ¿qué ocurrirá a partir de este momento? Si la población de leones llega a ser demasiado grande para el número de cebras en nuestra sabana, podrían acabar con todas, provocando su propia extinción por inanición. Pero incluso si el festín no es tan grande como para comerse todas las cebras, pero sí para dejar una población muy mermada, probablemente los leones tendrán que pasar hambre una buena temporada y algunos de ellos morirán hasta que las cebras tengan tiempo suficiente para reproducirse y volver a ser pasto de los leones. ¿Cuántas cebras morirán en el atracón? ¿Cuánto tiempo pasarán los leones hambre? ¿Cuántos morirán?

Continue reading

Liberado poliastro 0.2: Mecánica Orbital y Astrodinámica en Python

Después de meses de trabajo he liberado poliastro 0.2.0, una biblioteca Python y Fortran destinada a estudiar problemas de Mecánica Orbital y Astrodinámica en Python:

https://pybonacci.github.io/poliastro

La versión 0.1.0 nació en 2013 mientras estudiaba Orbital Mechanics en el Politecnico di Milano: tomé unas subrutinas escritas en Fortran por el profesor David A. Vallado para su libro "Fundamentals of Astrodynamics and Applications" y escribí una interfaz en Python para poder optimizar una transferencia entre la Tierra y Venus.

Sin embargo la biblioteca era muy engorrosa de utilizar y tuve muchos problemas a la hora de manejar cantidades con unidades. Inspirado por el paquete abandonado Plyades, decidí refactorizar drásticamente todo el código y el resultado es poliastro 0.2.

Gracias al módulo astropy.units es sencillo utilizar cantidades con unidades que se integran de manera casi transparente con NumPy. Además, he incluido un módulo para representar órbitas en dos dimensiones con matplotlib y he cambiado la forma en la que se usa la biblioteca.

Continue reading

Dibujando una rosa de frecuencias (reloaded)

Esta entrada es una actualización a la entrada Dibujando una rosa de frecuencias dónde se rehace el código para usar nuevas funcionalidades de matplotlib que simplifica el script.
Imaginaos que estáis de vacaciones en Agosto en la playa y la única preocupación que tenéis es observar las nubes. Como sois un poco frikis y no podéis desconectar de vuestra curiosidad científica decidís apuntar las ocurrencias de la procedencia de las nubes y al final de las vacaciones decidís representar esos datos. La forma más normal de hacerlo sería usando una rosa de frecuencias.
Primero de todo vamos a importar los módulos que nos harán falta:
In [2]:
import sys

import numpy as np
import matplotlib.pyplot as plt
import matplotlib
import math

%matplotlib inline

print('Versión de Python usada: ', sys.version)
print('Versión de Numpy usada: ', np.__version__)
print('Versión de Matplotlib usada: ', matplotlib.__version__)
Versión de Python usada:  3.4.1 (v3.4.1:c0e311e010fc, May 18 2014, 10:38:22) [MSC v.1600 32 bit (Intel)]
Versión de Numpy usada:  1.8.1
Versión de Matplotlib usada:  1.3.1

A continuación creamos nuestra muestra de datos totalmente inventada:
In [3]:
## Creamos un conjunto de datos
datos = np.arange(10,90,10)
## Los datos los queremos en tanto por ciento
datos = datos * 100. / datos.sum()
## Direcciones en radianes empezando por el N
## A las direcciones les restamos 22.5º para que las barras
## estén centradas exactamente en 0, 45, 90,...
direcciones = (np.arange(0, 360, 45) - 22.5) * math.pi / 180.
sectores = ['N','NE','E','SE','S','SW','W','NW']
En el bloque anterior de código, lo único que hemos hecho es crear un conjunto de datos sin sentido y los hemos separado en 8 intervalos que pretenden ser las 8 direcciones de donde provienen las nubes empezando por el Norte y en el sentido de las agujas del reloj. Finalmente los datos los expresamos como frecuencia en tanto por ciento en cada una de las 8 direcciones.
Matplotlib nos permite hacer gráficos polares pero estos gráficos están pensados para gráficos en sentido contrario a las agujas del reloj y empezando a las tres en punto (o al este). Por ello debemos modificar como se verán los datos en el gráfico polar. Para ello definimos el tipo de gráfico, colocamos el nombre de la dirección en cada sector definido (en este caso hemos usado 8 sectores), ponemos un título a nuestro gráfico y hemos acabado.
In [4]:
fig = plt.figure(figsize = (10,10))
ax = fig.add_subplot(111, polar=True)
## La siguiente línea de código hace que los datos vayan en el 
## sentido de las agujas del reloj
ax.set_theta_direction(-1)
## La siguiente línea de código coloca el 'origen' de la rotación
## donde le indiquemos, en este caso em Norte.
ax.set_theta_zero_location('N')
## Título
ax.set_title('Procedencia de las nubes en agosto (%)')
## Dibujamos los datos
ax.bar(direcciones, datos)
## Colocamos las etiquetas del eje x
ax.set_thetagrids(np.arange(0, 360, 45), sectores, frac = 1.1, fontsize = 10)
Out[4]:
(<a list of 16 Line2D ticklines objects>,
 <a list of 8 Text major ticklabel objects>)
Y listo.
Saludos.

Pandas (VI)

Y mucho más

Esto solo ha sido un pequeño vistazo con cosas que considero importantes pero que no tienen que ser las más importantes. Podéis echarle un ojo a:

  • sort, max, min, head, tail, unique, groupby, apply, transform, stack, unstack, mean, std, isnull, value_counts, notnull, rank, dropna, fillna, describe, cov, corr, duplicated, drop, pivot, pivot_table, drop_duplicates, quantile,...

para seguir viendo cosas útiles.

Finalmente, después de haceros sufrir con el formateo del código dentro del wordpress os he dejado un notebook en el github de Pybonacci donde tenéis todo lo que hemos visto en esta serie además de un pequeño caso práctico de aplicación.

Pandas (V)

Antes de nada, el contexto, para esta serie de entradas se va a usar lo siguiente:

Versión de Python:      3.3.1 (default, Apr 10 2013, 19:05:32) 
[GCC 4.6.3]
Versión de Pandas:      0.13.1
Versión de Numpy:       1.8.1
Versión de Matplotlib:  1.3.1

 

Y sin más preámbulos seguimos con esta quinta parte de la serie.

Unir (merge/join)

Pandas dispone de la función merge (documentación oficial) que permite 'unir' datos al estilo de como se hace con bases de datos relacionales (usando SQL). También se puede acceder al método merge disponible en las instancias a un Dataframe.

Por su parte, join es un método disponible en un DataFrame y sirve para hacer uniones de índices sobre índices o de índices sobre columnas. Las uniones que hace join las hace sobre los índices, en lugar de hacerlo sobre columnas comunes como se hace con merge. A ver si viendo los ejemplos queda un poco mejor este último párrafo y las diferencias entre join y merge.

Las uniones pueden ser uno-a-uno, muchos-a-uno o muchos-a-muchos.

Una unión uno-a-uno sería cuando unimos dos tablas (DataFrames) con índices únicos como hemos hecho en la entrega anterior con las concatenaciones.

datos1 = pd.DataFrame(np.random.randn(10), columns = ['columna1'])
datos2 = pd.DataFrame(np.random.randn(14), columns = ['columna2'], index = np.arange(1,15))
datos1j = datos1.join(datos2)
datos2j = datos2.join(datos1)
print('datos1j n{}n'.format(datos1j))
print('datos2j n{}'.format(datos2j))
datos1j 
   columna1  columna2
0 -0.209303       NaN
1 -0.430892  1.052453
2  0.766200 -0.346896
3  1.773694 -0.249700
4 -2.259187 -0.588739
5 -0.930647  0.160590
6  0.029990  0.421446
7  0.812770 -0.315913
8  0.681786  0.256745
9 -0.115109  0.524278

[10 rows x 2 columns]

datos2j 
    columna2  columna1
1   1.052453 -0.430892
2  -0.346896  0.766200
3  -0.249700  1.773694
4  -0.588739 -2.259187
5   0.160590 -0.930647
6   0.421446  0.029990
7  -0.315913  0.812770
8   0.256745  0.681786
9   0.524278 -0.115109
10 -1.707269       NaN
11 -1.140342       NaN
12 -1.751337       NaN
13 -0.481319       NaN
14  1.604800       NaN

[14 rows x 2 columns]

 

En los anteriores ejemplos, datos1j es el resultado de unir los datos datos2 a los datos datos1 en todos los índices comunes que tienen ambos teniendo solo en cuenta el rango de índices definido en datos1. Si algún dato en datos2 no tiene un índice presente en datos1 se rellenará con un NaN. Con datos2j sucede lo mismo que con datos1j lo que el índice que tiene relevancia ahora es el perteneciente a datos2j. No sé si habrá quedado más o menos claro.

Ahora vamos a unir pero usando la palabra clave how que nos permite decir como se van a tener en cuenta los índices. Normalmente le pasaremos el parámetro outer o inner. El primero, outer, indica que los índices de los DataFrames se unen como en una unión de conjuntos, el segundo, inner, une los índices como si hiciéramos una intersección de conjuntos. Veamos un par de ejemplos para que se vea de forma práctica, el primero usando outer y el segundo usando inner:

datos3j1 = datos1.join(datos2, how = 'outer')
datos3j2 = datos2.join(datos1, how = 'outer')
print('datos3j1 n{}n'.format(datos3j1))
print('datos3j2 recolocadosn{}n'.format(datos3j2.ix[:, ['columna1','columna2']]))
print('datos3j2 n{}'.format(datos3j2))
datos3j1 
    columna1  columna2
0  -0.209303       NaN
1  -0.430892  1.052453
2   0.766200 -0.346896
3   1.773694 -0.249700
4  -2.259187 -0.588739
5  -0.930647  0.160590
6   0.029990  0.421446
7   0.812770 -0.315913
8   0.681786  0.256745
9  -0.115109  0.524278
10       NaN -1.707269
11       NaN -1.140342
12       NaN -1.751337
13       NaN -0.481319
14       NaN  1.604800

[15 rows x 2 columns]

datos3j2 recolocados
    columna1  columna2
0  -0.209303       NaN
1  -0.430892  1.052453
2   0.766200 -0.346896
3   1.773694 -0.249700
4  -2.259187 -0.588739
5  -0.930647  0.160590
6   0.029990  0.421446
7   0.812770 -0.315913
8   0.681786  0.256745
9  -0.115109  0.524278
10       NaN -1.707269
11       NaN -1.140342
12       NaN -1.751337
13       NaN -0.481319
14       NaN  1.604800

[15 rows x 2 columns]

datos3j2 
    columna2  columna1
0        NaN -0.209303
1   1.052453 -0.430892
2  -0.346896  0.766200
3  -0.249700  1.773694
4  -0.588739 -2.259187
5   0.160590 -0.930647
6   0.421446  0.029990
7  -0.315913  0.812770
8   0.256745  0.681786
9   0.524278 -0.115109
10 -1.707269       NaN
11 -1.140342       NaN
12 -1.751337       NaN
13 -0.481319       NaN
14  1.604800       NaN

[15 rows x 2 columns]
datos4j1 = datos1.join(datos2, how = 'inner')
datos4j2 = datos2.join(datos1, how = 'inner')
print('datos4j1 n{}n'.format(datos4j1))
print('datos4j2 recolocadosn{}n'.format(datos4j2.ix[:, ['columna1','columna2']]))
print('datos4j2 n{}'.format(datos4j2))
datos4j1 
   columna1  columna2
1 -0.430892  1.052453
2  0.766200 -0.346896
3  1.773694 -0.249700
4 -2.259187 -0.588739
5 -0.930647  0.160590
6  0.029990  0.421446
7  0.812770 -0.315913
8  0.681786  0.256745
9 -0.115109  0.524278

[9 rows x 2 columns]

datos4j2 recolocados
   columna1  columna2
1 -0.430892  1.052453
2  0.766200 -0.346896
3  1.773694 -0.249700
4 -2.259187 -0.588739
5 -0.930647  0.160590
6  0.029990  0.421446
7  0.812770 -0.315913
8  0.681786  0.256745
9 -0.115109  0.524278

[9 rows x 2 columns]

datos4j2 
   columna2  columna1
1  1.052453 -0.430892
2 -0.346896  0.766200
3 -0.249700  1.773694
4 -0.588739 -2.259187
5  0.160590 -0.930647
6  0.421446  0.029990
7 -0.315913  0.812770
8  0.256745  0.681786
9  0.524278 -0.115109

[9 rows x 2 columns]

 

Todo lo anterior se puede hacer también usando la función o método merge pero encuentro que es una forma un poco más rebuscada por lo que no la vamos a mostrar aquí ya que añade complejidad. Veremos usos de merge más adelante.

Ahora vamos a mostrar una unión muchos-a-uno. Estas uniones se hacen sobre una o más columnas como referencia, no a partir de índices, por lo que los valores contenidos pueden no ser únicos. Como siempre, vamos a ver un poco de código para ver si clarifica un poco más la teoría:

datos1 = pd.DataFrame(np.random.randn(10), columns = ['columna1'])
datos1['otra_columna'] = ['hola', 'mundo'] * 5
datos2 = pd.DataFrame(np.random.randn(2,2), columns = ['col1', 'col2'], index = ['hola', 'mundo'])
print('datos1 n {} n'.format(datos1))
print('datos2 n {} n'.format(datos2))
print(u'Unión de datos n {} n'.format(datos1.join(datos2, on = 'otra_columna')))
datos1 
    columna1 otra_columna
0 -2.086230         hola
1 -1.015736        mundo
2 -0.919460         hola
3  0.923531        mundo
4 -0.445977         hola
5  0.719787        mundo
6  1.064480         hola
7 -0.235803        mundo
8  1.395844         hola
9  1.492875        mundo

[10 rows x 2 columns] 

datos2 
            col1      col2
hola   0.400267 -0.678126
mundo  0.855735  0.619193

[2 rows x 2 columns] 

Unión de datos 
    columna1 otra_columna      col1      col2
0 -2.086230         hola  0.400267 -0.678126
1 -1.015736        mundo  0.855735  0.619193
2 -0.919460         hola  0.400267 -0.678126
3  0.923531        mundo  0.855735  0.619193
4 -0.445977         hola  0.400267 -0.678126
5  0.719787        mundo  0.855735  0.619193
6  1.064480         hola  0.400267 -0.678126
7 -0.235803        mundo  0.855735  0.619193
8  1.395844         hola  0.400267 -0.678126
9  1.492875        mundo  0.855735  0.619193

[10 rows x 4 columns]

 

Estamos uniendo sobre los valores de la columna del DataFrame datos1 que presenta valores presentes en los índices del DataFrame datos2. En el anterior ejemplo hemos unido teniendo en cuenta una única columna, si queremos unir teniendo en cuenta varias columnas, el DataFrame que se le pase deberá presentar un MultiÍndice con tantos índices como columnas usemos (ver documentación sobre MultiÍndices y sobre unión con ellos).

Para hacer uniones de muchos-a-muchos usaremos merge que ofrece mayor libertad para poder hacer uniones de cualquier tipo (también las que hemos visto hasta ahora de uno-a-uno y de muchos-a-uno).

En el siguiente ejemplo vamos a hacer una unión de dos DataFrames usando merge y luego iremos explicando lo que hemos estado haciendo poco a poco para ver si se entiende un poco mejor.

datos_dcha = pd.DataFrame({'clave': ['foo'] * 3, 'valor_dcha': np.arange(3)})
datos_izda = pd.DataFrame({'clave': ['foo'] * 3, 'valor_izda': np.arange(5, 8)})
datos_unidos = pd.merge(datos_izda, datos_dcha, on = 'clave')
print('datos_dcha n {} n'.format(datos_dcha))
print('datos_izda n {} n'.format(datos_izda))
print('datos_unidos n {}'.format(datos_unidos))
datos_dcha 
   clave  valor_dcha
0   foo           0
1   foo           1
2   foo           2

[3 rows x 2 columns] 

datos_izda 
   clave  valor_izda
0   foo           5
1   foo           6
2   foo           7

[3 rows x 2 columns] 

datos_unidos 
   clave  valor_izda  valor_dcha
0   foo           5           0
1   foo           5           1
2   foo           5           2
3   foo           6           0
4   foo           6           1
5   foo           6           2
6   foo           7           0
7   foo           7           1
8   foo           7           2

[9 rows x 3 columns]

 

Vemos que si hacemos una unión de la anterior forma, a cada valor de datos_dcha le 'asocia' cada uno de los valores de datos_izda que tengan la misma clave. En la siquiente celda de código vemos otro ejemplo de lo anterior un poco más completo teniendo en cuenta dos columnas de claves y usando el método outer de 'unión':

datos_dcha = pd.DataFrame({'clave1': ['foo', 'foo', 'bar', 'bar'],
                           'clave2': ['one', 'one', 'one', 'two'],
                           'val_dcha': [4, 5, 6, 7]})
datos_izda = pd.DataFrame({'clave1': ['foo', 'foo', 'bar'],
                           'clave2': ['one', 'two', 'one'],
                           'val_izda': [1, 2, 3]})
datos_unidos = pd.merge(datos_izda, datos_dcha, how='outer')
print('datos_dcha n {} n'.format(datos_dcha))
print('datos_izda n {} n'.format(datos_izda))
print('datos_unidos n {}'.format(datos_unidos))
datos_dcha 
   clave1 clave2  val_dcha
0    foo    one         4
1    foo    one         5
2    bar    one         6
3    bar    two         7

[4 rows x 3 columns] 

datos_izda 
   clave1 clave2  val_izda
0    foo    one         1
1    foo    two         2
2    bar    one         3

[3 rows x 3 columns] 

datos_unidos 
   clave1 clave2  val_izda  val_dcha
0    foo    one         1         4
1    foo    one         1         5
2    foo    two         2       NaN
3    bar    one         3         6
4    bar    two       NaN         7

[5 rows x 4 columns]

 

Otra vez hemos llegado al final. ¡¡Estad atentos a la última entrega!!

Pandas (IV)

Antes de nada, el contexto, para esta serie de entradas se va a usar lo siguiente:

Versión de Python:      3.3.1 (default, Apr 10 2013, 19:05:32) 
[GCC 4.6.3]
Versión de Pandas:      0.13.1
Versión de Numpy:       1.8.1
Versión de Matplotlib:  1.3.1

 

Y sin más preámbulos seguimos con esta cuarta parte de la serie.

Concatenando datos

Para concatenar ficheros se usa la función pd.concat (documentación oficial]. Un ejemplo rápido sería el siguiente:

datos1 = pd.DataFrame(np.random.randn(5,3))
datos2 = pd.DataFrame(np.random.randn(5,3))
piezas = [datos1, datos2]
datos_concatenados_a = pd.concat(piezas)
print('datos1n {}'.format(datos1))
print('datos2n {}'.format(datos2))
print('datos_concatenadosn {}'.format(datos_concatenados_a))

Cuyo resultado sería:

datos1
          0         1         2
0 -1.691985 -1.181241 -0.714437
1  0.955094 -0.238498  1.137918
2 -0.533739 -0.285976 -0.990184
3 -0.626446  0.664830  0.278803
4 -0.183818 -0.013190  0.505786

[5 rows x 3 columns]
datos2
          0         1         2
0 -2.063044  2.328388  0.043275
1 -1.720170 -0.039871  0.954244
2 -0.173751  0.047003 -0.979577
3 -0.293044  1.928332 -1.323554
4  0.705127  3.711652 -0.535096

[5 rows x 3 columns]
datos_concatenados
          0         1         2
0 -1.691985 -1.181241 -0.714437
1  0.955094 -0.238498  1.137918
2 -0.533739 -0.285976 -0.990184
3 -0.626446  0.664830  0.278803
4 -0.183818 -0.013190  0.505786
0 -2.063044  2.328388  0.043275
1 -1.720170 -0.039871  0.954244
2 -0.173751  0.047003 -0.979577
3 -0.293044  1.928332 -1.323554
4  0.705127  3.711652 -0.535096

[10 rows x 3 columns]

Interesante, rápido y limpio, como me gusta. Pero, si nos fijamos, tenemos un problema con los índices ya que algunos están repetidos. Si accedemos al índice 0, por ejemplo, obtendríamos dos filas de valores en lugar de una.

datos_concatenados_a.ix[0]
              0            1            2
0     -1.691985    -1.181241    -0.714437
0     -2.063044     2.328388     0.043275

2 rows × 3 columns

Lo anterior podría llevar a equívocos. Esto lo podemos solventar de varias formas. Una sería reescribiendo la columna de índices para que no haya malentendidos al hacer cualquier operación. Por ejemplo:

datos_concatenados_aa = datos_concatenados_a
datos_concatenados_aa.index = range(datos_concatenados_aa.shape[0])
print('datos_concatenadosn {}'.format(datos_concatenados_aa))
datos_concatenados
          0         1         2
0 -1.691985 -1.181241 -0.714437
1  0.955094 -0.238498  1.137918
2 -0.533739 -0.285976 -0.990184
3 -0.626446  0.664830  0.278803
4 -0.183818 -0.013190  0.505786
5 -2.063044  2.328388  0.043275
6 -1.720170 -0.039871  0.954244
7 -0.173751  0.047003 -0.979577
8 -0.293044  1.928332 -1.323554
9  0.705127  3.711652 -0.535096

[10 rows x 3 columns]

O usando la palabra clave ignore_index pasándole el valor True al crear la concatenación. Por ejemplo:

datos_concatenados_aa = pd.concat(piezas, ignore_index = True)
print(datos_concatenados_aa)
          0         1         2
0 -1.691985 -1.181241 -0.714437
1  0.955094 -0.238498  1.137918
2 -0.533739 -0.285976 -0.990184
3 -0.626446  0.664830  0.278803
4 -0.183818 -0.013190  0.505786
5 -2.063044  2.328388  0.043275
6 -1.720170 -0.039871  0.954244
7 -0.173751  0.047003 -0.979577
8 -0.293044  1.928332 -1.323554
9  0.705127  3.711652 -0.535096

[10 rows x 3 columns]

Vale, hemos solventado el anterior problema pero que pasa si, por la razón que sea, nos interesase conservar los índices originales. Podríamos usar palabras clave para cada 'cosa' concatenada en el DataFrame final. Ejemplo:

#datos1 = pd.DataFrame(np.random.randn(5,3))
#datos2 = pd.DataFrame(np.random.randn(5,3))
#piezas = [datos1, datos2]
datos_concatenados_b = pd.concat(piezas, keys = ['datos1', 'datos2'])
print('datos1n {}'.format(datos1))
print('datos2n {}'.format(datos2))
print('datos_concatenadosn {}'.format(datos_concatenados_b))
datos1
          0         1         2
0 -1.691985 -1.181241 -0.714437
1  0.955094 -0.238498  1.137918
2 -0.533739 -0.285976 -0.990184
3 -0.626446  0.664830  0.278803
4 -0.183818 -0.013190  0.505786

[5 rows x 3 columns]
datos2
          0         1         2
0 -2.063044  2.328388  0.043275
1 -1.720170 -0.039871  0.954244
2 -0.173751  0.047003 -0.979577
3 -0.293044  1.928332 -1.323554
4  0.705127  3.711652 -0.535096

[5 rows x 3 columns]
datos_concatenados
                 0         1         2
datos1 0 -1.691985 -1.181241 -0.714437
       1  0.955094 -0.238498  1.137918
       2 -0.533739 -0.285976 -0.990184
       3 -0.626446  0.664830  0.278803
       4 -0.183818 -0.013190  0.505786
datos2 0 -2.063044  2.328388  0.043275
       1 -1.720170 -0.039871  0.954244
       2 -0.173751  0.047003 -0.979577
       3 -0.293044  1.928332 -1.323554
       4  0.705127  3.711652 -0.535096

[10 rows x 3 columns]

Vemos que hay índices repetidos pero están en 'grupos' diferentes. De esta forma, si queremos acceder a la fila con índice 0 del primer grupo de datos concatenados (datos1) podemos hacer lo siguiente:

print(datos_concatenados_b.ix['datos1'].ix[0])
<pre>0   -1.691985
1   -1.181241
2   -0.714437
Name: 0, dtype: float64</pre>
Estamos viendo filas, pero podemos hacer los mismo para las columnas, por supuesto, usando el nombre de la columna (en el ejemplo siguiente, la columna 0):
[code language="Python"]
print(datos_concatenados_b.ix['datos1'][0])
0   -1.691985
1    0.955094
2   -0.533739
3   -0.626446
4   -0.183818
Name: 0, dtype: float64

Vemos qué tipo de índice es este índice 'compuesto' que hemos creado:

datos_concatenados_b.index
MultiIndex(levels=[['datos1', 'datos2'], [0, 1, 2, 3, 4]],
labels=[[0, 0, 0, 0, 0, 1, 1, 1, 1, 1], [0, 1, 2, 3, 4, 0, 1, 2, 3, 4]])

Vemos que es un MultiIndex. No vamos a ver mucho más pero os lo dejo anotado para que sepáis que existen combinaciones de índices (o de columnas) y se manejan de forma un poco más compleja que un índice 'simple'. Se conoce como indexación jerárquica y permiten ser un poco más descriptivos (verbose) con nuestros DataFrames aunque conlleva un punto más de complejidad a la hora de trabajar con los datos.

¿Qué pasa cuando una de las columnas no es igual en los grupos de datos que queramos concatenar? El nuevo DataFrame tendrá en cuenta este aspecto rellenando con NaNs donde convenga. Veamos el siguiente código de ejemplo:

datos1 = pd.DataFrame(np.random.randn(5,3))
datos2 = pd.DataFrame(np.random.randn(5,4))
piezas = [datos1, datos2]
datos_concatenados_c = pd.concat(piezas, ignore_index = True)
print('datos1n {}'.format(datos1))
print('datos2n {}'.format(datos2))
print('datos_concatenadosn {}'.format(datos_concatenados_c))
datos1
          0         1         2
0 -0.082729 -0.016452 -1.280156
1  0.606336 -0.504770 -2.017690
2 -2.147009 -0.632275  0.023689
3 -0.255461 -0.042007  0.661835
4  2.351576  0.735611 -0.187072

[5 rows x 3 columns]
datos2
          0         1         2         3
0 -0.223023  0.070622 -0.577119 -1.430177
1 -1.661289 -0.214221  0.709818 -0.642611
2 -0.098368 -0.489105 -1.373906 -2.104431
3  0.880578 -0.601151 -1.450542 -0.289738
4 -1.461346 -0.539262  0.327825 -0.944431

[5 rows x 4 columns]
datos_concatenados
          0         1         2         3
0 -0.082729 -0.016452 -1.280156       NaN
1  0.606336 -0.504770 -2.017690       NaN
2 -2.147009 -0.632275  0.023689       NaN
3 -0.255461 -0.042007  0.661835       NaN
4  2.351576  0.735611 -0.187072       NaN
5 -0.223023  0.070622 -0.577119 -1.430177
6 -1.661289 -0.214221  0.709818 -0.642611
7 -0.098368 -0.489105 -1.373906 -2.104431
8  0.880578 -0.601151 -1.450542 -0.289738
9 -1.461346 -0.539262  0.327825 -0.944431

[10 rows x 4 columns]

Vemos que el primer grupo de datos, datos1, solo tiene tres columnas mientras que el segundo grupo, datos2, tiene 4 columnas. El resultado final tendrá en cuenta esto y rellenerá la columna 3 que pertenece a los datos del primer grupo de datos, datos1. Cool!

Lo visto hasta ahora para concatenar Series o DataFrames lo podemos hacer también usando el método append. Veamos un ejemplo similar a lo anterior:

datos1 = pd.DataFrame(np.random.randn(5,3))
datos2 = pd.DataFrame(np.random.randn(5,4))
datos_concatenados_d = datos1.append(datos2, ignore_index = True)
print('datos1n {}'.format(datos1))
print('datos2n {}'.format(datos2))
print('datos_concatenadosn {}'.format(datos_concatenados_d))
datos1
          0         1         2
0 -0.974367  1.732370  0.354479
1 -0.021746  2.215287  1.107243
2  0.018506  1.301015  1.103651
3 -1.857281 -1.181981  0.097104
4 -0.595689  0.140885  1.993213

[5 rows x 3 columns]
datos2
          0         1         2         3
0 -0.211180 -0.093403  0.215210 -0.154284
1  0.206997  1.277379 -0.893895 -0.216731
2 -1.138390 -0.067240  1.688928 -2.191215
3  0.938069  0.174496 -1.722735 -0.873746
4  0.177425  0.823896 -0.595673 -0.426416

[5 rows x 4 columns]
datos_concatenados
          0         1         2         3
0 -0.974367  1.732370  0.354479       NaN
1 -0.021746  2.215287  1.107243       NaN
2  0.018506  1.301015  1.103651       NaN
3 -1.857281 -1.181981  0.097104       NaN
4 -0.595689  0.140885  1.993213       NaN
5 -0.211180 -0.093403  0.215210 -0.154284
6  0.206997  1.277379 -0.893895 -0.216731
7 -1.138390 -0.067240  1.688928 -2.191215
8  0.938069  0.174496 -1.722735 -0.873746
9  0.177425  0.823896 -0.595673 -0.426416

[10 rows x 4 columns]

Otra vez hemos llegado al final. ¡¡Estad atentos a la próxima entrega!!