Python Book p300
Python Book p300
Python
para análisis de datos
Manipulación de datos ocn padas, NumPy y Jupyter
Sobre el autor
Wes McKinney es desarrollador de software y empresario en Nashville,
Tennessee. Tras obtener su título universitario en matemáticas en el
Massachussets Institute of Technology (MIT) en 2007, empezó a trabajar en
finanzas y economía cuantitativa en la compañía AQR Capital Management
en Greenwich, Connecticut. Frustrado por las incómodas herramientas de
análisis de datos que existían en ese momento, aprendió Python e inició lo
que más tarde se convertiría en el proyecto pandas. Es un miembro activo
de la comunidad de datos de Python y es defensor del uso de Python en
análisis de datos, finanzas y aplicaciones de computación científica.
Posteriormente, Wes fue cofundador y director ejecutivo de DataPad,
cuyas instalaciones tecnológicas y personal fueron adquiridos por Cloudera
en 2014. Desde entonces ha estado muy implicado en la tecnología Big
Data y se ha unido a los comités de administración de los proyectos Apache
Arrow y Apache Parquet en la Apache Software Foundation (ASF). En
2018 fundó Usra Labs, una organización sin ánimo de lucro centrada en el
desarrollo de Apache Arrow, en asociación con RStudio y Two Sigma
Investments. En 2021 ha creado la startup tecnológica Voltron Data, donde
trabaja en la actualidad como director de tecnología.
Agradecimientos
Esta obra es producto de muchos años de fructíferas discusiones,
colaboraciones y ayuda de muchas personas de todo el mundo. Me gustaría
dar las gracias a algunos de ellos.
Sobre el autor
Sobre la imagen de cubierta
Agradecimientos
En memoria de John D. Hunter (1968-2012)
Agradecimientos por la tercera edición (2022)
Agradecimientos por la segunda edición (2017)
Agradecimientos por la primera edición (2012)
Prefacio
Convenciones empleadas en este libro
Uso del código de ejemplo
Capítulo 1. Preliminares
1.1 ¿De qué trata este libro?
¿Qué tipos de datos?
1.2 ¿Por qué Python para análisis de datos?
Python como elemento de unión
Resolver el problema de «los dos lenguajes»
¿Por qué no Python?
1.3 Librerías esenciales de Python
NumPy
pandas
matplotlib
IPython y Jupyter
SciPy
scikit-learn
statsmodels
Otros paquetes
1.4 Instalación y configuración
Miniconda en Windows
GNU/Linux
Miniconda en macOS
Instalar los paquetes necesarios
Entornos de desarrollo integrados y editores de texto
1.5 Comunidad y conferencias
1.6 Navegar por este libro
Códigos de ejemplo
Datos para los ejemplos
Convenios de importación
Créditos
Prefacio
Quizá haya gente que describa buena parte del contenido del libro como «manipulación de datos» a diferencia
de «análisis de datos». También emplearemos los términos «disputa» (wrangling) o «procesado» (munging)
para referirnos a la manipulación de datos.
• Datos tabulares o en forma de hoja de cálculo, en los que cada columna puede ser
de un tipo distinto (cadena de texto, numérico, fecha u otro). Incluye la mayoría de
los tipos de datos almacenados normalmente en bases de datos relacionales o en
archivos de texto delimitados por tabuladores o comas.
• Arrays multidimensionales (matrices).
• Tablas múltiples de datos interrelacionados por columnas clave (lo que serían
claves primarias o externas para un usuario de SQL).
• Series temporales espaciadas uniformemente o de manera desigual.
Sin duda, esta no es una lista completa. Aunque no siempre pueda ser obvio, a un gran
porcentaje de conjuntos de datos se le puede dar una forma estructurada, más adecuada
para análisis y modelado de datos. Si no, puede ser posible extraer características de un
conjunto de datos para darles una forma estructurada. Como ejemplo, se podría procesar
una colección de artículos de prensa hasta convertirla en una tabla de frecuencia de
palabras, que se puede emplear después para realizar análisis de opiniones.
A la mayoría de los usuarios de programas de hoja de cálculo como Microsoft Excel,
quizá la herramienta de análisis de datos más utilizada en todo el mundo, no les resultarán
raros estos tipos de datos.
NumPy
NumPy (https://numpy.org), abreviatura de Numerical Python (Python numérico),
ha sido durante mucho tiempo la piedra angular de la computación numérica en Python.
Ofrece las estructuras de datos, los algoritmos y el «pegamento» necesario para la
mayoría de las aplicaciones científicas que tienen que ver con datos numéricos en Python.
NumPy contiene, entre otras cosas:
Más allá de las rápidas habilidades de proceso de arrays que NumPy le incorpora a
Python, otro de sus usos principales en análisis de datos es como contenedor para pasar
datos entre algoritmos y librerías. Para datos numéricos, los arrays de NumPy son más
eficaces para almacenar y manipular datos que las otras estructuras de datos integradas en
Python. Además, las librerías escritas en un lenguaje de bajo nivel, como C o FORTRAN,
pueden trabajar con los datos almacenados en un array de NumPy sin copiar datos en otra
representación de memoria distinta. De esta forma, muchas herramientas de cálculo
numérico para Python, o bien admiten los arrays de NumPy como estructura de datos
principal, o bien se centran en la interoperabilidad con NumPy.
pandas
pandas (https://pandas.pydata.org) ofrece estructuras de datos y funciones de alto
nivel diseñadas para flexibilizar el trabajo con datos estructurados o tabulares. Desde su
nacimiento en 2010, ha reforzado a Python como un potente y productivo entorno de
análisis de datos. Los objetos principales de pandas que se utilizarán en este libro son
DataFrame, una estructura de datos tabular y orientada a columnas con etiquetas de fila y
columna, y Series, un objeto array con etiquetas y unidimensional.
La librería pandas fusiona las ideas de NumPy sobre cálculo de arrays con el tipo de
habilidades de manipulación de datos que se pueden encontrar en hojas de cálculo y bases
de datos relacionales (como SQL). Ofrece una cómoda funcionalidad de indexado para
poder redimensionar, segmentar, realizar agregaciones y seleccionar subconjuntos de
datos. Como la manipulación, preparación y limpieza de los datos es una habilidad tan
importante en análisis de datos, pandas es uno de los focos de atención principales de este
libro.
Para poner al lector en antecedentes, empecé a crear pandas a principios de 2008,
durante el tiempo que estuve en AQR Capital Management, una empresa de
administración de inversiones cuantitativas. En aquel momento, tenía una serie de
requisitos muy claros que no estaban siendo bien resueltos por ninguna herramienta de las
que tenía a mi disposición:
Mi idea era poder hacer todas estas cosas de una sola vez, preferiblemente en un
lenguaje adecuado para el desarrollo de software genérico. Python era un buen candidato
para ello, pero en ese momento no había un conjunto integrado de estructuras de datos y
herramientas que ofrecieran esta funcionalidad. Como resultado de haber sido creado
inicialmente para resolver problemas analíticos financieros y empresariales, pandas
cuenta con una funcionalidad de series temporales especialmente profunda y con
herramientas idóneas para trabajar con datos indexados en el tiempo y generados por
procesos empresariales.
Me pasé buena parte de 2011 y 2012 ampliando las habilidades de pandas con la ayuda
de dos de mis primeros compañeros de trabajo de AQR, Adam Klein y Chang She. En
2013, dejé de estar tan implicado en el desarrollo diario de proyectos, y desde entonces
pandas se ha convertido en un proyecto propiedad por completo de su comunidad y
mantenido por ella, con más de 2000 colaboradores únicos en todo el mundo.
A los usuarios del lenguaje R de cálculos estadísticos, el nombre DataFrame les
resultará familiar, ya que el objeto se denominó así por el objeto data.frame de R. A
diferencia de Python, los marcos de datos o data frames están integrados en el lenguaje
de programación R y en su librería estándar. Como resultado de ello, muchas funciones
de pandas suelen ser parte de la implementación esencial de R, o bien son proporcionadas
por paquetes adicionales.
El propio nombre pandas deriva de panel data, un término de econometría que define
conjuntos de datos estructurados y multidimensionales, y también un juego de palabras
con la expresión inglesa «Python data analysis» (análisis de datos de Python).
matplotlib
matplotlib (https://matplotlib.org) es la librería de Python más conocida para
producir gráficos y otras visualizaciones de datos bidimensionales. Fue creada
originalmente por John D. Hunter, y en la actualidad un nutrido equipo de desarrolladores
se encarga de mantenerla. Fue diseñada para crear gráficos adecuados para su
publicación. Aunque hay otras librerías de visualización disponibles para programadores
Python, matplotlib sigue siendo muy utilizada y se integra razonablemente bien con el
resto del ecosistema. Creo que es una opción segura como herramienta de visualización
predeterminada.
IPython y Jupyter
El proyecto IPython (https://ipython.org) se inició en 2001 como proyecto
secundario de Fernando Pérez para crear un mejor intérprete de Python interactivo. En los
siguientes 20 años se ha convertido en una de las herramientas más importantes de la
moderna pila de datos de Python. Aunque no ofrece por sí mismo herramientas
computacionales o para análisis de datos, IPython se ha diseñado tanto para desarrollo de
software como para computación interactiva. Aboga por un flujo de trabajo ejecución-
exploración, en lugar del típico flujo editar-compilar-ejecutar de muchos otros lenguajes
de programación. También proporciona acceso integrado al shell y al sistema de archivos
del sistema operativo de cada usuario, lo que reduce la necesidad de cambiar entre una
ventana de terminal y una sesión de Python en muchos casos. Como buena parte de la
codificación para análisis de datos implica exploración, prueba y error, además de
repetición, IPython puede lograr que todo este trabajo se haga mucho más rápido.
En 2014, Fernando y el equipo de IPython anunciaron el proyecto Jupyter
(https://jupyter.org), una iniciativa más amplia para diseñar herramientas de
computación interactiva para cualquier tipo de lenguaje. El notebook (cuaderno) de
IPython basado en la web se convirtió en Jupyter Notebook, que ahora dispone de soporte
de más de 40 lenguajes de programación. El sistema IPython puede emplearse ahora
como kernel (un modo de lenguaje de programación) para utilizar Python con Jupyter. El
propio IPython se ha convertido en un componente del proyecto de fuente abierta Jupyter
mucho más extenso, que ofrece un entorno productivo para computación interactiva y
exploratoria. Su «modo» más antiguo y sencillo es un shell de Python diseñado para
acelerar la escritura, prueba y depuración del código Python. También se puede usar el
sistema IPython a través de Jupyter Notebook.
El sistema Jupyter Notebook también permite crear contenidos en Markdown y HTML
y proporciona un medio para crear documentos enriquecidos con código y texto.
Personalmente, yo utilizo IPython y Jupyter habitualmente en mi trabajo con Python,
ya sea ejecutando, depurando o probando código.
En el material contenido en GitHub que acompaña al libro
(https://github.com/wesm/pydata-book) se podrán encontrar notebooks de Jupyter que
contienen todos los ejemplos de código de cada capítulo. Si no es posible acceder a
GitHub, se puede probar el duplicado en Gitee (https://gitee.com/wesmckinn/pydata-
book).
SciPy
SciPy (https://scipy.org) es una colección de paquetes que resuelve una serie de
problemas de base en la ciencia computacional. Estas son algunas de las herramientas que
contienen sus distintos módulos:
• scipy.integrate: Rutinas de integración numéricas y distintos resolutores de
ecuaciones.
• scipy.linalg: Rutinas de álgebra lineal y descomposiciones de matrices que van
más allá de los proporcionados por numpy.linalg.
• scipy.optimize: Optimizadores (minimizadores) de funciones y algoritmos de
búsqueda de raíces.
• scipy.signal: Herramientas de procesamiento de señal.
• scipy.sparse: Matrices dispersas y resolutores de sistemas lineales dispersos.
• scipy.special: Contenedor de SPECFUN, una librería de FORTRAN que
implementa muchas funciones matemáticas comunes, como la función gamma.
• scipy.stats: Distribuciones de probabilidad estándares continuas y discretas
(funciones de densidad, muestreadores, funciones de distribución continua),
diversas pruebas estadísticas y más estadísticas descriptivas.
scikit-learn
Desde los inicios del proyecto en 2007, scikit-learn (https://scikit-learn.org) se
ha convertido en el principal juego de herramientas de aprendizaje automático de uso
general para programadores de Python. En el momento de escribir esto, más de 2000
personas han contribuido con código al proyecto. Incluye submódulos para modelos
como:
Otros paquetes
Ahora, en 2022, hay muchas otras librerías de Python de las que se podría hablar en un
libro sobre ciencia de datos. Entre ellas se incluyen varios proyectos de reciente creación,
como TensorFlow o PyTorch, que se han hecho populares para trabajar con aprendizaje
automático o inteligencia artificial. Ahora que ya hay otros libros en el mercado que
tratan específicamente esos proyectos, yo recomendaría utilizar este libro para crear una
buena base en manipulación de datos genérica en Python. Tras su lectura, es muy
probable que ya se esté bien preparado para pasar a un recurso más avanzado que pueda
presuponer un cierto nivel de experiencia.
Miniconda en Windows
Para empezar en Windows, descargue el instalador de Miniconda para la última
versión de Python disponible (ahora mismo 3.9) de la página https://conda.io.
Recomiendo seguir las instrucciones de instalación para Windows disponibles en el sitio
web de conda, que quizá hayan cambiado entre el momento en que se publicó este libro y
el momento en el que esté leyendo esto. La mayoría de la gente querrá la versión de 64
bits, pero si no funciona en su máquina Windows, puede instalar sin problemas la versión
de 32 bits.
Cuando le pregunten si desea realizar la instalación solo para usted o para todos los
usuarios de su sistema, elija la opción más adecuada para usted. La instalación individual
bastará para seguir el libro. También le preguntarán si desea añadir Miniconda a la
variable de entorno PATH del sistema. Si dice que sí (yo normalmente lo hago), entonces
esta instalación de Miniconda podría anular otras versiones de Python que pudiera tener
instaladas. Si contesta que no, entonces tendrá que utilizar el atajo del menú de inicio de
Windows que se haya instalado para poder utilizar este Miniconda. Dicha entrada podría
llamarse algo así como «Anaconda3 (64-bit)».
Supondré que no ha añadido Miniconda al PATH de su sistema. Para verificar que las
cosas estén correctamente configuradas, abra la entrada «Anaconda Prompt
(Miniconda3)» dentro de «Anaconda3 (64-bit)» en el menú Inicio. A continuación,
intente lanzar el intérprete Python escribiendo python. Debería aparecer un mensaje
como este:
(base) C:\Users\Wes>python
Python 3.9 [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32
Type “help”, “copyright”, “credits” or “license” for more information.
>>>
Para salir del shell de Python, escriba el comando exit() y pulse Intro.
GNU/Linux
Los detalles de Linux variarán dependiendo del tipo de distribución Linux que se
tenga; aquí daré información para distribuciones como Debian, Ubuntu, CentOS y
Fedora. La configuración es similar a la de macOS, con la excepción de cómo esté
instalado Miniconda. La mayoría de los lectores descargarán el archivo instalador de 64
bits predeterminado, que es para arquitectura x86 (pero es posible que en el futuro más
usuarios tengan máquinas Linux basadas en aarch64). El instalador es un shell-script que
se debe ejecutar en el terminal. Entonces dispondrá de un archivo con un nombre
parecido a Miniconda3-latest-Linux-x86_64.sh. Para instalarlo, ejecute este fragmento
de código con bash:
$ bash Miniconda3-latest-Linux-x86_64.sh
Ciertas distribuciones de Linux incluirán en sus administradores todos los paquetes de Python necesarios (aunque
versiones obsoletas, en algunos casos), y se pueden instalar usando una herramienta como apt. La configuración
descrita aquí utiliza Miniconda, ya que se puede reproducir mucho más fácilmente en las distintas distribuciones
y resulta más sencillo actualizar paquetes a sus versiones más recientes.
>>>
Para salir del shell de Python, teclee exit() y pulse Intro o Control-D.
Miniconda en macOS
Descargue el instalador de Miniconda para macOS, cuyo nombre debería ser algo así
como Miniconda3-latest-MacOSX-arm64.sh para ordenadores macOS con Apple
Silicon lanzados del 2020 en adelante, o bien Miniconda3-latest-MacOSX-x86_64.sh
para Macs con Intel lanzados antes de 2020. Abra la aplicación Terminal de macOS e
instale ejecutando el instalador (lo más probable en su directorio de descargas) con bash.
$ bash $HOME/Downloads/Miniconda3-latest-MacOSX-arm64.sh
>>>
Para salir del shell, pulse Control-D o teclee exit() y pulse Intro.
Ahora crearemos un nuevo “entorno” conda mediante el comando conda create que
utiliza Python 3.10:
(base) $ conda create -y -n pydata-book python=3.10
Es necesario utilizar conda activate para activar el entorno cada vez que se abra un nuevo terminal. Puede
ver información sobre el entorno activo de conda en cualquier momento desde el terminal utilizando el
comando conda info.
A continuación instalaremos los paquetes esenciales empleados a lo largo del libro
(junto con sus dependencias) con conda install:
(pydata-book) $ conda install -y pandas jupyter matplotlib
Utilizaremos también otros paquetes, pero pueden instalarse más tarde, cuando sean
necesarios. Hay dos formas de instalar paquetes, con conda install y con pip install.
Siempre es preferible conda install al trabajar con Miniconda, pero algunos paquetes no
están disponibles en conda, de modo que si conda install $nombre_paquete falla,
pruebe con pip install $nombre_paquete.
Si quiere instalar todos los paquetes utilizados en el resto del libro, puede hacerlo ya ejecutando:
En Windows, para indicar la continuación de línea ponga un carácter ^ en lugar de la barra invertida \
empleada en Linux y macOS.
Aunque se pueden utilizar tanto conda como pip para instalar paquetes, conviene evitar actualizar paquetes
instalados originalmente con conda utilizando pip (y viceversa), ya que hacer esto puede dar lugar a problemas
en el entorno. Recomiendo quedarse con conda si es posible, volviendo a pip solo para paquetes que no estén
disponibles con conda install.
• pydata: Una lista de grupos de Google para cuestiones relacionadas con Python
para análisis de datos y pandas.
• pystatsmodels: para preguntas relacionadas con statsmodels o pandas.
• Lista de correo generalmente para scikit-learn ([email protected]) y
aprendizaje automático en Python.
• numpy-discussion: para cuestiones relacionadas con NumPy.
• scipy-user: para preguntas generales sobre SciPy o Python científico.
Códigos de ejemplo
La mayoría de los códigos de ejemplo del libro se muestran con entrada y salida, como
si aparecieran ejecutados en el shell de IPython o en notebooks de Jupyter:
In [5]: EJEMPLO DE CÓDIGO
Out[5]: SALIDA
$ ls
appa.ipynb ch05.ipynb ch09.ipynb ch13.ipynb README.md
He hecho todo lo que estaba en mi mano para asegurar que el repositorio GitHub
contiene todo lo necesario para reproducir los ejemplos, pero quizá haya cometido errores
u omisiones. Si es así, le pido por favor que me envíe un email a:
[email protected].
La mejor manera de informar de errores hallados en el libro es consultando la página
de erratas del libro original en el sitio web de O’Reilly
(https://www.oreilly.com/catalog/errata.csp?isbn=0636920519829).
Convenios de importación
La comunidad Python ha adoptado distintos convenios de nomenclatura para los
módulos más utilizados:
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
import statsmodels as sm
Esto significa que cuando vea np.arange, se trata de una referencia a la función
arange de NumPy. Esto es así porque se considera mala praxis en desarrollo de software
de Python importarlo todo (from numpy import *) de un paquete de gran tamaño como
NumPy.
Capítulo 2
Fundamentos del lenguaje Python, IPython y Jupyter Notebooks
Cuando escribí la primera edición de este libro en 2011 y 2012, existían menos recursos para
aprender a realizar análisis de datos en Python. Esto era en parte un problema del tipo «huevo y
gallina»; muchas librerías que ahora damos por sentadas, como pandas, scikit-learn y statsmodels,
eran entonces inmaduras comparándolas con lo que son ahora. En la actualidad existe cada vez más
literatura sobre ciencia de datos, análisis de datos y aprendizaje automático, que complementa los
trabajos previos sobre computación científica genérica destinados a científicos computacionales,
físicos y profesionales de otros campos de investigación. También hay libros excelentes para aprender
el lenguaje de programación Python y convertirse en un ingeniero de software eficaz.
Como este libro está destinado a ser un texto introductorio para el trabajo con datos en Python, me
parece valioso disponer de una visión de conjunto de algunas de las funciones más importantes de las
estructuras integradas en Python y de sus librerías desde el punto de vista de la manipulación de
datos. Por esta razón, solo presentaré en este capítulo y el siguiente la información suficiente para que
sea posible seguir el resto del libro.
Buena parte de este libro se centra en analítica de tablas y herramientas de preparación de datos
para trabajar con conjuntos de datos lo bastante reducidos como para poder manejarse en un
ordenador personal. Para utilizar estas herramientas, en ocasiones es necesario hacer ciertas
modificaciones para organizar datos desordenados en un formato tabular (o estructurado) más
sencillo de manejar. Por suerte, Python es un lenguaje ideal para esto. Cuanto mayor sea la capacidad
de manejo por parte del usuario del lenguaje Python y sus tipos de datos integrados, más fácil será
preparar nuevos conjuntos de datos para su análisis.
Algunas de las herramientas de este libro se exploran mejor desde una sesión activa de IPython o
Jupyter. En cuanto aprenda a iniciar IPython y Jupyter, le recomiendo que siga los ejemplos, de modo
que pueda experimentar y probar distintas cosas. Al igual que con un entorno de consola con teclado,
desarrollar una buena memoria recordando los comandos habituales también forma parte de la curva
de aprendizaje.
Hay conceptos introductorios de Python que este capítulo no trata, como, por ejemplo, las clases y la programación orientada a
objetos, que quizá encuentre útil en su incursión en el análisis de datos con Python.
Para intensificar sus conocimientos del lenguaje Python, le recomiendo que complemente este capítulo con el tutorial oficial de
Python (https://docs.python.org) y posiblemente con uno de los muchos libros de calidad que existen sobre programación
genérica con Python. Algunas recomendaciones para empezar son las siguientes:
El símbolo >>> que se puede ver en el código es el prompt en el que se escriben las expresiones y
fragmentos de código. Para salir del intérprete de Python, se puede escribir exit() o pulsar Control-D
(únicamente en Linux y macOS).
Ejecutar programas de Python es tan sencillo como llamar a python con un archivo .py como
primer argumento. Supongamos que habíamos creado hello_world.py con este contenido:
print(“Hello world”)
Se puede ejecutar utilizando el siguiente comando (el archivo hello_world.py debe estar en su
directorio de trabajo actual del terminal):
$ python hello_world.py
Hello world
Mientras algunos programadores ejecutan su código Python de esta forma, los que realizan
análisis de datos o ciencia computacional emplean IPython, un intérprete de Python mejorado, o bien
notebooks de Jupyter, cuadernos de código basados en la web y creados inicialmente dentro del
proyecto IPython. Ofreceré en este capítulo una introducción al uso de IPython y Jupyter, y en el
apéndice A profundizaré más en la funcionalidad de IPython. Al utilizar el comando %run, IPython
ejecuta el código del archivo especificado dentro del mismo proceso, y permite así explorar los
resultados de forma interactiva cuando ha terminado:
$ ipython
Python 3.10.4 | packaged by conda-forge | (main, Mar 24 2022, 17:38:57)
Type ‘copyright’, ‘credits’ or ‘license’ for more information
IPython 7.31.1 — An enhanced Interactive Python. Type ‘?’ for help.
In [2]:
El prompt predeterminado de IPython adopta el estilo numerado In [2]:, a diferencia del prompt
estándar >>>.
In [1]: a = 5
In [2]: a
Out[2]: 5
In [7]: data
Out[7]:
[-0.20470765948471295,
0.47894333805754824,
-0.5194387150567381,
-0.55573030434749,
1.9657805725027142,
1.3934058329729904,
0.09290787674371767]
Las dos primeras líneas son sentencias de código Python; la segunda sentencia crea una variable
llamada data que se refiere a un diccionario Python de reciente creación. La última línea imprime el
valor de data en la consola.
Muchos tipos de objetos Python están formateados para que sean más legibles, o queden mejor al
imprimirlos, que es distinto de la impresión normal que se consigue con print. Si se imprimiera la
variable data anterior en el intérprete de Python estándar, sería mucho menos legible:
>>> import numpy as np
>>> data = [np.random.standard_normal() for i in range(7)]
>>> print(data)
>>> data
[-0.5767699931966723, -0.1010317773535111, -1.7841005313329152,
-1.524392126408841, 0.22191374220117385, -1.9835710588082562,
-1.6081963964963528]
IPython ofrece además formas de ejecutar bloques arbitrarios de código (mediante una especie de
método copiar y pegar con pretensiones) y fragmentos enteros de código Python. Se puede utilizar el
notebook de Jupyter para trabajar con bloques de código más grandes, como veremos muy pronto.
file:///home/wesm/.local/share/jupyter/runtime/nbserver-185259-open.html
http://localhost:8888/?token=0a77b52fefe52ab83e3c35dff8de121e4...
or http://127.0.0.1:8888/?token=0a77b52fefe52ab83e3c35dff8de121e4...
Muchas personas utilizan Jupyter como entorno local, pero también se puede desplegar en servidores y se puede acceder a él
remotamente. No daré aquí más detalles al respecto, pero le animo a que explore este tema en Internet si le resulta relevante
para sus necesidades.
Figura 2.1. Página de inicio de Jupyter notebook.
Para crear un nuevo notebook, haga clic en el botón New (nuevo) y seleccione la opción Python
3. Debería verse algo parecido a lo que muestra la figura 2.2. Si es su primera vez, pruebe a hacer clic
en la celda vacía y escriba una línea de código Python. A continuación, pulse Mayús-Intro para
ejecutarlo.
Al guardar el notebook utilizando Save and Checkpoint (guardar y comprobar) en el menú File
(archivo), se crea un archivo con la extensión .ipynb. Se trata de un formato de archivo autónomo
que incluye todo lo que contiene en ese momento el notebook (incluyendo el resultado de código ya
evaluado). Estos archivos pueden ser abiertos y editados por otros usuarios de Jupyter.
Para renombrar un notebook abierto, haga clic en su título en la parte superior de la página y
escriba el nuevo, pulsando Enter al terminar.
Para abrir un notebook existente, ponga el archivo en el mismo directorio en el que inició el
proceso del notebook (o en una subcarpeta contenida en él) y después haga clic en el nombre desde la
página de inicio. Puede probar con los notebooks de mi repositorio wesm/pydata-book de GitHub;
consulte además la figura 2.3.
Cuando quiera cerrar un notebook, haga clic en el menú File (archivo) y elija Close and Halt
(cerrar y detener). Si solamente cierra la pestaña del navegador, el proceso de Python asociado al
notebook se mantendrá en funcionamiento en segundo plano.
Aunque el notebook de Jupyter pueda parecer una experiencia distinta al shell de IPython, casi
todos los comandos y herramientas de este capítulo se pueden utilizar en ambos entornos.
Autocompletado
A primera vista, el shell de IPython parece una versión en apariencia distinta al intérprete de
Python estándar (que se abre con python). Una de las principales mejoras con respecto al shell de
Python estándar es el autocompletado, que puede encontrarse en muchos IDE u otros entornos de
análisis computacional interactivos. Mientras se escriben expresiones en el shell, pulsar la tecla Tab
buscará en el espacio de nombres cualquier variable (objetos, funciones, etc.) que coincida con los
caracteres que se han escrito hasta ahora y mostrará los resultados en un cómodo menú desplegable:
In [1]: an_apple = 27
In [2]: an_example = 42
In [3]: an<Tab>
an_apple an_example any
En este ejemplo se puede observar que IPython mostró las dos variables que definí, además de la
función integrada any. Además, es posible completar métodos y atributos de cualquier objeto tras
escribir un punto:
In [3]: b = [1, 2, 3]
In [4]: b.<Tab>
In [2]: datetime.<Tab>
Hay que tener en cuenta que IPython oculta por defecto métodos y atributos que empiezan por el carácter de subrayado, como por
ejemplo métodos mágicos y métodos y atributos «privados» internos, para evitar desordenar la pantalla (y confundir a los usuarios
principiantes). Estos también se pueden autocompletar, pero primero hay que escribir un subrayado para verlos. Si prefiere ver
siempre estos métodos en el autocompletado, puede cambiar la opción en la configuración de IPython. Consulte la documentación
de IPython (https://ipython.readthedocs.io/en/stable/) para averiguar cómo hacerlo.
In [2]: b?
Type: list
String form: [1, 2, 3]
Length: 3
Docstring:
Built-in mutable sequence.
In [3]: print?
Docstring:
print(value, ..., sep=’ ‘, end=’\n’, file=sys.stdout, flush=False)
In [6]: add_numbers?
Signature: add_numbers(a, b)
Docstring:
Add two numbers together
Returns
———
the_sum : type of arguments
File: <ipython-input-9-6a548a216e27>
Type: function
El signo de interrogación tiene un último uso, destinado a buscar en el espacio de nombres de
IPython de una manera similar a la línea de comandos estándar de Unix o Windows. Una serie de
caracteres combinados con el carácter comodín (*) mostrará todos los nombres que coinciden con la
expresión comodín. Por ejemplo, podríamos obtener una lista de todas las funciones del espacio de
nombres de alto nivel de NumPy que contengan load:
In [9]: import numpy as np
In [10]: np.*load*?
np.__loader__
np.load
np.loads
np.loadtxt
Sangrado, no llaves
Python emplea espacios en blanco (tabuladores o espacios) para estructurar el código, en lugar de
utilizar llaves, como en muchos otros lenguajes como R, C++, Java y Perl. Veamos un bucle for de
un algoritmo de ordenación:
for x in array:
if x < pivot:
less.append(x)
else:
greater.append(x)
El signo de los dos puntos denota el comienzo de un bloque de código sangrado, tras el cual todo
el código debe estar sangrado en la misma cantidad hasta el final del bloque.
Les guste o no, el espacio en blanco con significado es una realidad en la vida de los
programadores de Python. Aunque pueda parecer raro al principio, es de esperar que con el tiempo
uno se acostumbre.
Recomiendo enérgicamente reemplazar el tabulador por cuatro espacios como sangrado predeterminado. Muchos editores de
texto incluyen un parámetro en su configuración que reemplaza los tabuladores por espacios de manera automática (¡actívelo!).
Los notebooks de IPython y Jupyter insertarán automáticamente cuatro espacios en líneas nuevas después de dos puntos y
sustituirán también los tabuladores por cuatro espacios.
Como ha podido ver hasta ahora, las sentencias de Python no tienen que terminar tampoco por
punto y coma. No obstante, el punto y coma se puede utilizar para separar varias sentencias que están
en una sola línea:
a = 5; b = 6; c = 7
Normalmente no se ponen varias sentencias en una sola línea en Python, porque puede hacer que
el código sea menos legible.
Todo es un objeto
Comentarios
results = []
for line in file_handle:
# deja las líneas vacías por ahora
# if len(line) == 0:
# continúa
results.append(line.replace(“foo”, “bar”))
Los comentarios pueden aparecer también tras una línea de código ejecutado. Aunque algunos
programadores prefieren colocar los comentarios en la línea que precede a una determinada línea de
código, en ocasiones puede resultar útil:
Se llama a las funciones utilizando paréntesis y pasándoles cero o más argumentos, asignando de
manera opcional el valor devuelto a una variable:
result = f(x, y, z)
g()
Casi todos los objetos de Python tienen funciones asociadas, conocidas como métodos, que tienen
acceso al contenido interno del objeto. Se les puede llamar utilizando esta sintaxis:
obj.some_method(x, y, z)
Las funciones pueden admitir argumentos posicionales y de palabra clave:
result = f(a, b, c, d=5, e=”foo”)
Al asignar una variable (o nombre) en Python, se está creando una referencia al objeto que aparece
al lado derecho del signo igual. En términos prácticos, supongamos una lista de enteros:
In [8]: a = [1, 2, 3]
In [10]: b
Out[10]: [1, 2, 3]
En algunos lenguajes, la asignación de b hará que se copien los datos [1, 2, 3]. En Python, a y b
se refieren ahora en realidad al mismo objeto, la lista original [1, 2, 3] (véase una representación de
esto en la figura 2.5). Puede probarlo por sí mismo añadiendo un elemento a a y después examinando
b:
In [11]: a.append(4)
In [12]: b
Out[12]: [1, 2, 3, 4]
Comprender la semántica de las referencias de Python y cuándo, cómo y por qué se copian los
datos es especialmente importante cuando se trabaja con conjuntos de datos grandes en Python.
La asignación también se denomina vinculación, porque estamos asociando un nombre a un objeto. A los nombres de
variables que han sido asignados se les llama, asimismo, variables vinculadas.
Cuando se pasan objetos como argumentos a una función, se crean nuevas variables locales que
hacen referencia a los objetos originales sin copiar nada. Si se vincula un nuevo objeto a una variable
dentro de una función, no se sobreescribirá una variable del mismo nombre que esté en el «ámbito»
(o scope) exterior de la función (el «ámbito padre»). Por eso es posible modificar el interior de un
argumento mutable. Supongamos que tenemos la siguiente función:
Entonces tenemos:
In [14]: data = [1, 2, 3]
In [15]: append_element(data, 4)
In [16]: data
Out[16]: [1, 2, 3, 4]
Las variables en Python no tienen un tipo inherente asociado; una variable puede hacer referencia
a un tipo distinto de objeto simplemente haciendo una asignación. No hay problema con lo siguiente:
In [17]: a = 5
In [18]: type(a)
Out[18]: int
In [19]: a = “foo”
In [20]: type(a)
Out[20]: str
Las variables son nombres para objetos dentro de un espacio de nombres determinado; la
información del tipo se almacena en el propio objeto. Algunos observadores podrían concluir
apresuradamente que Python no es un «lenguaje de tipos». Pero esto no es cierto; veamos este
ejemplo:
In [21]: “5” + 5
——————————————————————————————————
En algunos lenguajes, la cadena de texto “5” se podría convertir de manera implícita en un entero,
produciendo así 10. En otros lenguajes, el entero 5 podría transformarse en una cadena de texto,
produciendo así el texto concatenado “55”. En Python, estas transformaciones implícitas no están
permitidas.
A este respecto, decimos que Python es un lenguaje de tipos fuertes, lo que significa que cada
objeto tiene un tipo (o clase) específico, y las conversiones implícitas solo se producen en
determinadas circunstancias permitidas, como las siguientes:
In [22]: a = 4.5
In [23]: b = 2
Aquí, incluso aunque b sea un entero, se convierte implícitamente en un tipo float para la
operación de división.
Conocer el tipo de un objeto es importante, y útil para poder escribir funciones que puedan
manejar muchos tipos distintos de entradas. Se puede comprobar que un objeto es una instancia de un
determinado tipo utilizando la función isinstance:
In [26]: a = 5
isinstance puede aceptar una tupla de tipos si queremos comprobar que el tipo de un objeto está
entre los presentes en la tupla:
In [28]: a = 5; b = 4.5
Atributos y métodos
Los objetos en Python suelen tener tanto atributos (otros objetos Python almacenados «dentro» del
objeto) como métodos (funciones asociadas a un objeto que pueden tener acceso a sus datos internos).
A ambos se accede mediante la sintaxis obj.attribute_name:
In [1]: a = “foo”
Duck typing
Es posible que con frecuencia no importe el tipo de un objeto, sino solamente si incluye
determinados métodos o tiene un cierto comportamiento. A esto se le denomina «duck typing», que se
podría traducir como «tipado de pato», aunque no tiene mucho sentido, porque se emplea esta
expresión por el dicho «si camina como un pato y grazna como un pato, entonces es un pato». Por
ejemplo, es posible verificar que un objeto es iterable si implementa el protocolo iterador. Para
muchos objetos, esto significa que tiene un «método mágico» __iter__, aunque una alternativa y
mejor forma de comprobarlo es intentar usar la función iter:
Esta función devolvería True para cadenas de texto, así como para la mayoría de los tipos de
colección de Python:
Importaciones
En Python, un módulo es simplemente un archivo con la extensión .py que contiene código
Python. Supongamos que tenemos el siguiente módulo:
# some_module.py
PI = 3.14159
def f(x):
return x + 2
def g(a, b):
return a + b
Si queremos acceder a las variables y funciones definidas en some_module.py, desde otro archivo
del mismo directorio podemos hacer esto:
import some_module
result = some_module.f(5)
pi = some_module.PI
O como alternativa:
from some_module import g, PI
result = g(5, PI)
Utilizando la palabra clave as se les puede asignar a las importaciones distintos nombres de
variable:
import some_module as sm
from some_module import PI as pi, g as gf
r1 = sm.f(pi)
r2 = gf(6, pi)
La mayor parte de las operaciones matemáticas binarias y las comparaciones utilizan la misma
sintaxis matemática ya conocida empleada en otros lenguajes de programación:
In [37]: 5 – 7
Out[37]: -2
In [38]: 12 + 21.5
Out[38]: 33.5
In [39]: 5 <= 2
Out[39]: False
Operación Descripción
a+b Suma a y b
a–b Resta b de a
a // b División de piso de a por b, es decir, calcula el cociente de la división entre a y b, sin tener en cuenta el resto
a ** b Eleva a a la potencia de b
a&b True si tanto a como and b son True; para enteros, toma AND bitwise (o a nivel de bit)
a^b Para booleanos, True si a o b es True, pero no ambos; para enteros, toma OR EXCLUSIVO bitwise
a == b True si a es igual a b
a != b True si a no es igual a b
Para comprobar si dos variables se refieren al mismo objeto, utilizamos la palabra clave is, que no
se puede usar para verificar que dos objetos no sean el mismo:
In [40]: a = [1, 2, 3]
In [41]: b = a
In [42]: c = list(a)
In [43]: a is b
Out[43]: True
In [44]: a is not c
Out[44]: True
Como la función list siempre crea una lista nueva de Python (por ejemplo, una copia), podemos
estar seguros de que c es distinto de a. Comparar con is no es lo mismo que utilizar el operador ==,
porque en este casi tenemos:
In [45]: a == c
Out[45]: True
Habitualmente se utilizan is e is not también para comprobar que una variable sea None, ya que
solamente hay una instancia de None:
In [46]: a = None
In [47]: a is None
Out[47]: True
Muchos objetos en Python, como listas, diccionarios, arrays NumPy y la mayoría de los tipos
(clases) definidos por el usuario, son mutables. Esto significa que el objeto o los valores que contiene
se pueden modificar:
In [48]: a_list = [“foo”, 2, [4, 5]]
In [50]: a_list
Out[50]: [‘foo’, 2, (3, 4)]
Otros, como cadenas de texto y tuplas, son inmutables, lo que significa que sus datos internos no
pueden cambiarse:
Conviene recordar que, simplemente porque el hecho de que se pueda mutar un objeto, no
significa que siempre se deba hacer. Estas acciones se conocen como efectos colaterales. Por ejemplo,
al escribir una función, cualquier efecto colateral debería ser explícitamente comunicado al usuario en
la documentación de la función o en los comentarios. Si es posible, recomiendo tratar de evitar
efectos colaterales y favorecer la inmutabilidad, incluso aunque pueda haber objetos mutables
implicados.
Tipos escalares
Python tiene un pequeño conjunto de tipos integrados para manejar datos numéricos, cadenas de
texto, valores booleanos (True o False) y fechas y horas. A estos tipos de “valores sencillos” se les
denomina tipos escalares; en este libro nos referiremos a ellos simplemente como escalares. Consulte
en la tabla 2.2 una lista de los principales tipos escalares. El manejo de fechas y horas se tratará de
manera individual, porque estos valores son suministrados por el módulo datetime de la librería
estándar.
Tipo Descripción
None El valor «null» de Python (solo existe una instancia del objeto None)
float Número de punto flotante de precisión doble (observe que no existe un tipo double distinto)
Tipos numéricos
Los principales tipos de Python para los números son int y float. Un int puede almacenar
números arbitrariamente grandes:
In [53]: ival = 17239871
In [54]: ival ** 6
Out[54]: 26254519291092456596965462913230729701102721
Los números de punto flotante se representan con el tipo float de Python. Internamente, cada uno
es un valor de precisión doble. También se pueden expresar con notación científica:
In [55]: fval = 7.243
In [56]: fval2 = 6.78e-5
La división de enteros que no dé como resultado otro número entero siempre producirá un número
de punto flotante:
In [57]: 3 / 2
Out[57]: 1.5
Para lograr una división de enteros al estilo de C (que no tiene en cuenta el resto si el resultado no
es un número entero), utilizamos el operador de división de piso //:
In [58]: 3 // 2
Out[58]: 1
Cadenas de texto
Mucha gente utiliza Python por sus capacidades internas de manejo de cadenas de texto. Se
pueden escribir literales de cadena empleando o bien comillas simples ‘ o dobles “ (en general se
utilizan más las dobles comillas):
a = ‘one way of writing a string’
b = “another way”
Quizá sorprenda el hecho de que esta cadena de texto c contenga realmente cuatro líneas de texto;
los saltos de línea después de “”” y después de lines están incluidos. Podemos contar los caracteres
de la nueva línea con el método count sobre c:
In [60]: c.count(“\n”)
Out[60]: 3
Para interpretar este mensaje de error, léalo de abajo a arriba. Hemos intentado reemplazar el
carácter («item») de la posición 10 por la letra “f”, pero esto no está permitido para objetos de cadena
de texto. Si necesitamos modificar una cadena de texto, tenemos que utilizar una función o un método
que cree una nueva cadena, como el método replace para cadenas de texto:
In [63]: b = a.replace(“string”, “longer string”)
In [64]: b
Out[64]: ‘this is a longer string’
Muchos objetos de Python se pueden transformar en una cadena de texto utilizando la función
str:
In [66]: a = 5.6
In [67]: s = str(a)
In [68]: print(s)
5.6
Las cadenas de texto son una secuencia de caracteres Unicode y, por lo tanto, se les puede tratar
igual que otras secuencias, como por ejemplo las listas y las tuplas:
In [69]: s = “python”
In [70]: list(s)
Out[70]: [‘p’, ‘y’, ‘t’, ‘h’, ‘o’, ‘n’]
In [71]: s[:3]
Out[71]: ‘pyt’
In [73]: print(s)
12\34
Si tenemos una cadena de texto con muchas barras invertidas y ningún carácter especial, podría
ser bastante molesto. Por suerte se puede poner delante de la primera comilla del texto una r, que
significa que los caracteres se deben interpretar tal y como están:
In [74]: s = r”this\has\no\special\characters”
In [75]: s
Out[75]: ‘this\\has\\no\\special\\characters’
In [78]: a + b
Out[78]: ‘this is the first half and this is the second half’
La creación de plantillas o formato de cadenas de texto es otro tema importante. Con la llegada de
Python 3, esto puede hacerse de más formas que antes, así que aquí describiremos brevemente la
mecánica de uno de los interfaces principales. Los objetos de cadena de texto tienen un método
format que se puede utilizar para sustituir argumentos formateados dentro de la cadena, produciendo
una nueva:
In [79]: template = “{0:.2f} {1:s} are worth US${2:d}”
En esta cadena:
• {0:.2f}significa formatear el primer argumento como un número de punto flotante con dos
decimales.
• {1:s} significa formatear el segundo argumento como una cadena de texto.
• {2:d} significa formatear el tercer argumento como un entero exacto.
Para sustituir los argumentos para estos parámetros de formato, pasamos una secuencia de
argumentos al método format:
In [80]: template.format(88.46, “Argentine Pesos”, 1)
Out[80]: ‘88.46 Argentine Pesos are worth US$1’
Python 3.6 introdujo una nueva característica llamada «cadenas-f» (abreviatura de «literales de
cadena formeateados») que puede lograr que sea aún más cómoda la creación de cadenas
formateadas.
Para crear una cadena-f, escribimos el carácter f justo antes de un literal de cadena. Dentro de la
propia cadena de texto, encerramos las expresiones Python en llaves para sustituir el valor de la
expresión por la cadena formateada:
In [81]: amount = 10
Pueden añadirse especificadores de formato tras cada expresión, utilizando la misma sintaxis que
hemos visto antes con las plantillas de cadena:
In [85]: f”{amount} {currency} is worth US${amount / rate:.2f}”
Out[85]: ‘10 Pesos is worth US$0.11’
El formato de cadenas de texto es un tema de gran profundidad; existen varios métodos y distintas
opciones y modificaciones disponibles para controlar cómo se formatean los valores en la cadena
resultante.
Bytes y Unicode
En el Python moderno (es decir, Python 3.0 y superior), Unicode se ha convertido en el tipo de
cadena de texto de primer orden en permitir un manejo más sólido de texto ASCII y no ASCII. En
versiones más antiguas de Python, las cadenas de texto eran todo bytes sin una codificación Unicode
explícita. Se podía convertir a Unicode suponiendo que se conociera la codificación del carácter.
Aquí muestro una cadena de texto Unicode de ejemplo con caracteres no ASCII:
In [86]: val = “español”
In [87]: val
Out[87]: ‘español’
Podemos convertir esta cadena Unicode en su representación de bytes UTF-8 utilizando el método
encode:
In [89]: val_utf8
Out[89]: b’espa\xc3\xb1ol’
In [90]: type(val_utf8)
Out[90]: bytes
Suponiendo que conocemos la codificación Unicode de un objeto bytes, podemos volver atrás
utilizando el método decode:
In [91]: val_utf8.decode(“utf-8”)
Out[91]: ‘español’
Aunque ahora se prefiere utilizar UTF-8 para cualquier codificación, por razones históricas es
posible encontrar datos en diferentes y variadas codificaciones:
In [92]: val.encode(“latin1”)
Out[92]: b’espa\xf1ol’
In [93]: val.encode(“utf-16”)
Out[93]: b’\xff\xfee\x00s\x00p\x00a\x00\xf1\x00o\x00l\x00’
In [94]: val.encode(“utf-16le”)
Out[94]: b’e\x00s\x00p\x00a\x00\xf1\x00o\x00l\x00’
Es más habitual encontrar objetos bytes cuando se trabaja con archivos, donde quizá no sea
deseable decodificar implícitamente todos los datos a cadenas Unicode.
Booleanos
Los dos valores booleanos de Python se escriben como True y False. Las comparaciones y otras
expresiones condicionales evalúan a True o False. Los valores booleanos se combinan con las
palabras clave and y or:
In [95]: True and True
Out[95]: True
In [98]: int(True)
Out[98]: 1
In [100]: b = False
In [101]: not a
Out[101]: False
In [102]: not b
Out[102]: True
Los tipos str, bool, int y float son también funciones que se pueden usar para convertir valores
a dichos tipos:
In [103]: s = “3.14159”
In [105]: type(fval)
Out[105]: float
In [106]: int(fval)
Out[106]: 3
In [107]: bool(fval)
Out[107]: True
In [108]: bool(0)
Out[108]: False
Observe que la mayoría de los valores que no son cero, cuando se convierten a bool, son True.
None
None es el tipo de valor de Python nulo o null.
In [109]: a = None
In [110]: a is None
Out[110]: True
In [111]: b = 5
Fechas y horas
El módulo datetime integrado de Python ofrece los tipos datetime, date y time. El tipo
datetime combina la información almacenada en date y time y es el más utilizado:
In [115]: dt.day
Out[115]: 29
In [116]: dt.minute
Out[116]: 30
Dada una instancia datetime, se pueden extraer los objetos equivalentes date y time llamando a
métodos de la datetime del mismo nombre:
In [117]: dt.date()
Out[117]: datetime.date(2011, 10, 29)
In [118]: dt.time()
Out[118]: datetime.time(20, 30, 21)
Las cadenas de texto se pueden convertir (analizar) en objetos datetime con la función strptime:
In [120]: datetime.strptime(“20091031”, “%Y%m%d”)
Out[120]: datetime.datetime(2009, 10, 31, 0, 0)
In [122]: dt_hour
Out[122]: datetime.datetime(2011, 10, 29, 20, 0)
Como datetime.datetime es un tipo inmutable, métodos como este siempre producen nuevos
objetos. Así, en el código de arriba, dt no es modificado por replace:
In [123]: dt
Out[123]: datetime.datetime(2011, 10, 29, 20, 30, 21)
In [126]: delta
Out[126]: datetime.timedelta(days=17, seconds=7179)
In [127]: type(delta)
Out[127]: datetime.timedelta
In [129]: dt + delta
Out[129]: datetime.datetime(2011, 11, 15, 22, 30)
Control de ujo
Python tiene varias palabras clave internas para lógica condicional, bucles y otros conceptos
estándares de control de flujo, que pueden encontrarse en otros lenguajes de programación.
La sentencia if es uno de los tipos de sentencia de control de flujo más conocidos. Comprueba
una condición que, si es True, evalúa el código del bloque que le sigue:
x = -5
if x < 0:
print(“It’s negative”)
Una sentencia if puede ir seguida de manera opcional por uno o más bloques elif y un bloque
multifuncional else si todas las condiciones son False:
if x < 0:
print(“It’s negative”)
elif x == 0:
print(“Equal to zero”)
elif 0 < x < 5:
print(“Positive but smaller than 5”)
else:
print(“Positive and larger than or equal to 5”)
Si alguna de las condiciones es True, no se alcanzará ningún bloque elif o else. Con una condición compuesta utilizando and o or, las
condiciones se evalúan de izquierda a derecha y se produce un cortocircuito:
In [130]: a = 5; b = 7
In [131]: c = 8; d = 4
En este ejemplo, la comparación c > d nunca resulta evaluada porque la primera comparación era
True.
También es posible encadenar comparaciones:
In [133]: 4 > 3 > 2 > 1
Out[133]: True
Bucles for
Los bucles for se utilizan para aplicar determinadas instrucciones a una colección (como una lista
o una tupla) o a un iterador. La sintaxis estándar de un bucle for es:
Se puede avanzar un bucle for a la siguiente repetición, saltando el resto del bloque, mediante la
palabra clave continue. Veamos este código, que suma enteros de una lista y omite valores None:
Un bucle for puede darse por terminado también con la palabra clave break. Este código suma
elementos de una lista hasta que se llega a un 5:
sequence = [1, 2, 0, 4, 6, 5, 2, 1]
total_until_5 = 0
for value in sequence:
if value == 5:
break
total_until_5 += value
La palabra clave break solo finaliza el bucle for más interno; los bucles for exteriores seguirán
ejecutándose:
Como veremos con más detalle, si los elementos de la colección o iterador son secuencias (tuplas
o listas, por ejemplo), se pueden desempaquetar cómodamente en variables de la sentencia del bucle
for:
for a, b, c in iterator:
# hace algo
Bucles while
Un bucle while especifica una condición y un bloque de código que se va ejecutar hasta que la
condición evalúe a False o el bucle sea finalizado explícitamente con break:
x = 256
total = 0
while x > 0:
if total > 500:
break
total += x
x = x // 2
pass
pass es la sentencia «no operativa» (es decir, que no hace nada) de Python. Se puede utilizar en
los bloques en los que no hay que realizar ninguna acción (o como marcador para código que aún no
se ha implementado); solo es necesario porque Python emplea el espacio en blanco para delimitar los
bloques:
if x < 0:
print(“negative!”)
elif x == 0:
# TODO: pone algo inteligente aquí
pass
else:
print(“positive!”)
range
In [136]: list(range(10))
Out[136]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Se puede dar un inicio, un final y un paso o incremento (que puede ser negativo):
In [137]: list(range(0, 20, 2))
Out[137]: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
Como se puede comprobar, range produce enteros hasta el valor final pero sin incluirlo; se suele
utilizar para aplicar instrucciones a distintas secuencias según un índice:
In [139]: seq = [1, 2, 3, 4]
Aunque se pueden utilizar funciones como list para almacenar todos los enteros generados por
range en alguna otra estructura de datos, a menudo la forma de iteración predeterminada será la que
el usuario decida. El siguiente fragmento de código suma todos los números de 0 a 99.999 que son
múltiplos de 3 o 5:
In [141]: total = 0
In [142]: for i in range(100_000):
.....: # % es el operador módulo
.....: if I % 3 == 0 or i % 5 == 0:
.....: total += i
In [143]: print(total)
2333316668
Aunque el rango generado puede ser arbitrariamente grande, el uso de la memoria en cualquier
momento determinado puede ser muy pequeño.
2.4 Conclusión
Este capítulo ha ofrecido una breve introducción a algunos conceptos básicos del lenguaje Python
y a los entornos de programación IPython y Jupyter. En el siguiente capítulo trataremos muchos tipos
de datos y funciones integradas y utilidades de entrada-salida que se emplearán continuamente en
todo el libro.
Capítulo 3
Estructuras de datos integrados, funciones y archivos
Tupla
Una tupla es una secuencia de objetos Python inmutable y de longitud
fija que, una vez asignada, no puede modificarse. La forma más sencilla de
crear una tupla es mediante una secuencia de valores separados por comas y
encerrados entre paréntesis:
In [2]: tup = (4, 5, 6)
In [3]: tup
Out[3]: (4, 5, 6)
In [5]: tup
Out[5]: (4, 5, 6)
In [8]: tup
Out[8]: (‘s’, ‘t’, ‘r’, ‘i’, ‘n’, ‘g’)
In [11]: nested_tup
Out[11]: ((4, 5, 6), (7, 8))
In [12]: nested_tup[0]
Out[12]: (4, 5, 6)
In [13]: nested_tup[1]
Out[13]: (7, 8)
Aunque los objetos almacenados en una tupla pueden ser mutables por sí
mismos, una vez la tupla se ha creado, ya no es posible cambiar qué objeto
está almacenado en cada espacio:
In [17]: tup
Out[17]: (‘foo’, [1, 2, 3], True)
Si se multiplica una tupla por un entero, como con las listas, se logra
concatenar tantas copias de la tupla como el resultado de la multiplicación:
In [19]: (‘foo’, ‘bar’) * 4
Out[19]: (‘foo’, ‘bar’, ‘foo’, ‘bar’, ‘foo’, ‘bar’, ‘foo’,
‘bar’)
Hay que tener en cuenta que los objetos como tales no se copian, solo las
referencias a ellos.
Desempaquetar tuplas
Si se intenta asignar una expresión de variables de estilo tupla, Python
intentará desempaquetar el valor situado a la derecha del signo igual:
In [20]: tup = (4, 5, 6)
In [21]: a, b, c = tup
In [22]: b
Out[22]: 5
In [25]: d
Out[25]: 7
b = tmp
In [27]: a
Out[27]: 1
In [28]: b
Out[28]: 2
In [29]: b, a = a, b
In [30]: a
Out[30]: 2
In [31]: b
Out[31]: 1
El desempaquetado de variables se suele utilizar para iterar sobre
secuencias de tuplas o listas:
In [36]: a
Out[36]: 1
In [37]: b
Out[37]: 2
In [38]: rest
Out[38]: [3, 4, 5]
Métodos de tupla
Como el tamaño y contenido de una tupla no se puede modificar, tiene
poca incidencia en los métodos de instancia. Uno especialmente útil
(disponible también en las listas) es count, que cuenta el número de
apariciones de un valor:
In [40]: a = (1, 2, 2, 2, 3, 4, 2)
In [41]: a.count(2)
Out[41]: 4
Listas
A diferencia de las tuplas, las listas tienen longitud variable y su
contenido se puede modificar. Las listas son mutables, y pueden definirse
utilizando paréntesis cuadrados [] o la función de tipo list:
In [42]: a_list = [2, 3, 7, None]
In [45]: b_list
Out[45]: [‘foo’, ‘bar’, ‘baz’]
In [47]: b_list
Out[47]: [‘foo’, ‘peekaboo’, ‘baz’]
Las listas y las tuplas son semánticamente similares (aunque las tuplas
no se pueden modificar) y ambas se pueden emplear en muchas funciones
de manera intercambiable.
La función integrada list se usa con frecuencia en proceso de datos
como una forma de materializar una expresión iteradora o generadora:
In [48]: gen = range(10)
In [49]: gen
Out[49]: range(0, 10)
In [50]: list(gen)
Out[50]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [52]: b_list
Out[52]: [‘foo’, ‘peekaboo’, ‘baz’, ‘dwarf’]
In [54]: b_list
Out[54]: [‘foo’, ‘red’, ‘peekaboo’, ‘baz’, ‘dwarf’]
In [56]: b_list
Out[56]: [‘foo’, ‘red’, ‘baz’, ‘dwarf’]
In [59]: b_list.remove(“foo”)
In [60]: b_list
Out[60]: [‘red’, ‘baz’, ‘dwarf’, ‘foo’]
Verificar que una lista contenga un valor es mucho más lento que hacerlo
con diccionarios y conjuntos (que presentaremos en breve), pues Python
realiza una exploración lineal por los valores de la lista, mientras que puede
revisar los otros (basados en tablas hash) en todo momento.
Igual que con las tuplas, sumar dos listas con + las concatena:
In [63]: [4, None, “foo”] + [7, 8, (2, 3)]
Out[63]: [4, None, ‘foo’, 7, 8, (2, 3)]
In [66]: x
Out[66]: [4, None, ‘foo’, 7, 8, (2, 3)]
everything = []
for chunk in list_of_lists:
everything.extend(chunk)
everything = []
for chunk in list_of_lists:
everything = everything + chunk
Ordenar
In [68]: a.sort()
In [69]: a
Out[69]: [1, 2, 3, 5, 7]
sort tiene varias opciones que de vez en cuando resultan útiles. Una de
ellas es la capacidad para pasar una clave de ordenación secundaria (es
decir, una función que produce un valor que se utiliza para ordenar los
objetos). Por ejemplo, podríamos ordenar una colección de cadenas de texto
por sus longitudes:
In [70]: b = [“saw”, “small”, “He”, “foxes”, “six”]
In [71]: b.sort(key=len)
In [72]: b
Out[72]: [‘He’, ‘saw’, ‘six’, ‘small’, ‘foxes’]
Corte o rebanado
In [74]: seq[1:5]
Out[74]: [2, 3, 7, 5]
In [76]: seq
Out[76]: [7, 2, 3, 6, 3, 6, 0, 1]
In [78]: seq[3:]
Out[78]: [6, 3, 6, 0, 1]
Los índices negativos cortan la secuencia relativa al final:
In [79]: seq[-4:]
Out[79]: [3, 6, 0, 1]
In [80]: seq[-6:-2]
Out[80]: [3, 6, 3, 6]
Un uso inteligente de esto es pasar -1, que tiene el útil efecto de revertir
una lista o tupla:
In [82]: seq[::-1]
Out[82]: [1, 0, 6, 3, 6, 3, 2, 7]
Diccionario
Puede que el diccionario o dict sea la estructura de datos integrada de
Python más importante de todas. En otros lenguajes de programación, a los
diccionarios también se les llama mapas hash o arrays asociativos. Un
diccionario almacena una colección de pares clave-valor, donde la clave y
el valor son objetos Python. Cada clave está asociada a un valor, de modo
que dicho valor se pueda recuperar, insertar, modificar o borrar
convenientemente dada una determinada clave. Una forma de crear un
diccionario es utilizando llaves {} y signos de dos puntos para separar
claves y valores:
In [83]: empty_dict = {}
In [85]: d1
Out[85]: {‘a’: ‘some value’, ‘b’: [1, 2, 3, 4]}
In [87]: d1
Out[87]: {‘a’: ‘some value’, ‘b’: [1, 2, 3, 4], 7: ‘an
integer’}
In [88]: d1[“b”]
Out[88]: [1, 2, 3, 4]
In [91]: d1
Out[91]:
{‘a’ : ‘some value’,
‘b’ : [1, 2, 3, 4],
7: ‘an integer’,
5: ‘some value’}
In [93]: d1
Out[93]:
{‘a’ : ‘some value’,
‘b’ : [1, 2, 3, 4],
7: ‘an integer’,
5: ‘some value’,
‘dummy’: ‘another value’}
In [95]: d1
Out[95]:
{‘a’ : ‘some value’,
‘b’ : [1, 2, 3, 4],
7: an integer’,
‘dummy’: ‘another value’}
In [97]: ret
Out[97]: ‘another value’
In [98]: d1
Out[98]: {‘a’: ‘some value’, ‘b’: [1, 2, 3, 4], 7: ‘an
integer’}
In [103]: d1
Out[103]: {‘a’: ‘some value’, ‘b’: ‘foo’, 7: ‘an integer’,
‘c’: 12}
mapping = {}
for key, value in zip(key_list, value_list):
mapping[key] = value
In [105]: tuples
Out[105]: <zip at 0x7fefe4553a00>
In [106]: mapping = dict(tuples)
In [107]: mapping
Out[107]: {0: 4, 1: 3, 2: 2, 3: 1, 4: 0}
Valores predeterminados
if key in some_dict:
value = some_dict[key]
else:
value = default_value
In [109]: by_letter = {}
In [111]: by_letter
Out[111]: {‘a’: [‘apple’, ‘atom’], ‘b’: [‘bat’, ‘bar’,
‘book’]}
In [114]: by_letter
Out[114]: {‘a’: [‘apple’, ‘atom’], ‘b’: [‘bat’, ‘bar’,
‘book’]}
In [123]: d
Out[123]: {(1, 2, 3): 5}
Conjunto o set
Un conjunto o set es una colección desordenada de elementos únicos. Se
pueden crear de dos maneras, mediante la función set o con un literal de
conjunto con llaves:
In [124]: set([2, 2, 2, 1, 3, 3])
Out[124]: {1, 2, 3}
In [125]: {2, 2, 2, 1, 3, 3}
Out[125]: {1, 2, 3}
In [127]: b = {3, 4, 5, 6, 7, 8}
In [129]: a | b
Out[129]: {1, 2, 3, 4, 5, 6, 7, 8}
In [130]: a.intersection(b)
Out[130]: {3, 4, 5}
In [131]: a & b
Out[131]: {3, 4, 5}
Si se pasa una entrada que no es un conjunto a métodos como union e intersection, Python
convertirá dicha entrada en un conjunto antes de ejecutar la operación. Al utilizar los
operadores binarios, ambos objetos deben ser ya conjuntos.
In [133]: c |= b
In [134]: c
Out[134]: {1, 2, 3, 4, 5, 6, 7, 8}
In [135]: d = a.copy()
In [136]: d &= b
In [137]: d
Out[137]: {3, 4, 5}
In [140]: my_set
Out[140]: {(1, 2, 3, 4)}
enumerate
index = 0
for value in collection:
# hace algo con value
index += 1
sorted
In [150]: list(zipped)
Out[150]: [(‘foo’, ‘one’), (‘bar’, ‘two’), (‘baz’, ‘three’)]
reversed
result = []
for value in collection:
if condition:
result.append(expr)
In [158]: unique_lengths
Out[158]: {1, 2, 3, 4, 6}
In [161]: loc_mapping
Out[161]: {‘a’: 0, ‘as’: 1, ‘bat’: 2, ‘car’: 3, ‘dove’: 4,
‘python’: 5}
Imaginemos que queremos conseguir una sola lista que contenga todos
los nombres con dos o más a. Podríamos sin duda hacer esto con un sencillo
bucle for:
In [163]: names_of_interest = []
In [165]: names_of_interest
Out[165]: [‘Maria’, ‘Natalia’]
In [167]: result
Out[167]: [‘Maria’, ‘Natalia’]
Hay que recordar que el orden de las expresiones for sería el mismo si
se escribiera un bucle for anidado en lugar de una comprensión de lista:
flattened = []
for tup in some_tuples:
for x in tup:
flattened.append(x)
Esto produce una lista de listas, en lugar de una lista reducida de todos
los elementos internos.
3.2 Funciones
Las funciones son el método principal y más importante de Python para
organizar y reutilizar código. Como regla general, si nos anticipamos a la
necesidad de repetir el mismo código o uno muy parecido más de una vez,
puede merecer la pena escribir una función que se pueda reutilizar. Las
funciones también pueden ayudar a que el código sea más legible dando un
nombre a un grupo de sentencias Python.
Las funciones se declaran con la palabra clave def. Una función
contiene un bloque de código, con un uso opcional de la palabra clave
return:
In [173]: def my_function(x, y):
.....: return x + y
In [176]: result
Out[176]: 3
In [179]: print(result)
None
def func():
a = []
for i in range(5):
a.append(i)
In [187]: a
Out[187]: [0, 1, 2, 3, 4]
In [188]: func()
In [189]: a
Out[189]: [0, 1, 2, 3, 4, 0, 1, 2, 3, 4]
[]
def f():
a = 5
b = 6
c = 7
return a, b, c
a, b, c = f()
En este caso, return_value sería una tupla de tres, conteniendo las tres
variables devueltas. Una alternativa a devolver varios valores del modo que
hemos visto, que quizá resulte interesante, sería devolver un diccionario:
def f():
a = 5
b = 6
c = 7
return {“a” : a, “b” : b, “c” : c}
import re
def clean_strings(strings):
result = []
for value in strings:
value = value.strip()
value = re.sub(“[!#?]”, “”, value)
value = value.title()
result.append(value)
return result
El resultado tiene este aspecto:
In [195]: clean_strings(states)
Out[195]:
[‘Alabama’,
‘Georgia’,
‘Georgia’,
‘Georgia’,
‘Florida’,
‘South Carolina’,
‘West Virginia’]
Un método alternativo que puede resultar útil es crear una lista de las
operaciones que se desean aplicar a un determinado conjunto de cadenas de
texto:
def remove_punctuation(value):
return re.sub(“[!#?]”, “”, value)
clean_ops = [str.strip, remove_punctuation, str.title]
def clean_strings(strings, ops):
result = []
for value in strings:
for func in ops:
value = func(value)
result.append(value)
return result
In [206]: strings
Out[206]: [‘aaaa’, ‘foo’, ‘abab’, ‘bar’, ‘card’]
Generadores
Muchos objetos en Python soportan iteración, como, por ejemplo, sobre
los objetos de una lista o sobre las líneas de un archivo. Esto se lleva a cabo
por medio del protocolo iterador, una forma genérica de hacer que los
objetos sean iterables. Por ejemplo, iterar sobre un diccionario produce las
claves de diccionario:
In [207]: some_dict = {“a”: 1, “b”: 2, “c”: 3}
In [210]: dict_iterator
Out[210]: <dict_keyiterator at 0x7fefe45465c0>
In [211]: list(dict_iterator)
Out[211]: [‘a’, ‘b’, ‘c’]
def squares(n=10):
print(f”Generating squares from 1 to {n ** 2}”)
for i in range(1, n + 1):
yield i ** 2
En realidad, cuando se llama al generador, no se ejecuta inmediatamente
ningún código:
In [213]: gen = squares()
In [214]: gen
Out[214]: <generator object squares at 0x7fefe437d620>
Como los generadores producen resultados de un elemento cada vez frente a una lista
entera de una sola vez, los programas en los que se emplean utilizan menos memoria.
Expresiones generadoras
In [217]: gen
Out[217]: <generator object <genexpr> at 0x7fefe437d000>
def _make_gen():
for x in range(100):
yield x ** 2
gen = _make_gen()
Las expresiones generadoras se pueden emplear en lugar de las
comprensiones de lista como argumentos de función en algunos casos:
In [218]: sum(x ** 2 for x in range(100))
Out[218]: 328350
Módulo itertools
Función Descripción
def attempt_float(x):
try:
return float(x)
except:
return x
In [228]: attempt_float(“something”)
Out[228]: ‘something’
def attempt_float(x):
try:
return float(x)
except ValueError:
return x
Entonces tenemos:
def attempt_float(x):
try:
return float(x)
except (TypeError, ValueError):
return x
En algunos casos, quizá no interese suprimir una excepción, pero sí que
se ejecute cierto código sin tener en cuenta si el código del bloque try
funciona o no.Para ello utilizamos finalmente:
f = open(path, mode=”w”)
try:
write_to_file(f)
finally:
f.close()
f = open(path, mode=”w”)
try:
write_to_file(f)
except:
print(“Failed”)
else:
print(“Succeeded”)
finally:
f.close()
Excepciones en IPython
for line in f:
print(line)
Las líneas salen del archivo con los marcadores de final de línea (EOL:
end-of-line) intactos, de modo que normalmente veremos código para
obtener una lista de líneas libre de EOL en un archivo como el siguiente:
In [235]: lines = [x.rstrip() for x in open(path,
encoding=”utf-8”)]
In [236]: lines
Out[236]:
[‘Sueña el rico en su riqueza,’,
‘que más cuidados le ofrece;’,
‘’,
‘sueña el pobre que padece’,
‘su miseria y su pobreza;’,
‘’,
‘sueña el que a medrar empieza,’,
‘sueña el que afana y pretende,’,
‘sueña el que agravia y ofende,’,
‘’,
‘y en el mundo, en conclusión,’,
‘todos sueñan lo que son,’,
‘aunque ninguno lo entiende.’,
‘’]
Modo Descripción
w Modo de solo escritura; crea un nuevo archivo (borrando los datos de cualquier archivo
con el mismo nombre)
x Modo de solo escritura; crea un nuevo archivo pero da error si la ruta del archivo ya
existe
b Se suma al modo para trabajar con archivos binarios (por ejemplo, "rb" o "wb")
In [240]: f1.read(10)
Out[240]: ‘Sueña el r’
In [242]: f2.read(10)
Out[242]: b’Sue\xc3\xb1a el ‘
In [244]: f2.tell()
Out[244]: 10
In [246]: sys.getdefaultencoding()
Out[246]: ‘utf-8’
Para obtener un comportamiento consistente a lo largo de las distintas
plataformas, es mejor pasar una codificación (como encoding=”utf-8”,
ampliamente usada) al abrir archivos.
seek cambia la posición del archivo al byte indicado en el mismo:
In [247]: f1.seek(3)
Out[247]: 3
In [248]: f1.read(1)
Out[248]: ‘ñ’
In [249]: f1.tell()
Out[249]: 5
In [251]: f2.close()
In [255]: lines
Out[255]:
[‘Sueña el rico en su riqueza,\n’,
‘que más cuidados le ofrece;\n’,
‘sueña el pobre que padece\n’,
‘su miseria y su pobreza;\n’,
‘sueña el que a medrar empieza,\n’,
‘sueña el que afana y pretende,\n’,
‘sueña el que agravia y ofende,\n’,
‘y en el mundo, en conclusión,\n’,
‘todos sueñan lo que son,\n’,
‘aunque ninguno lo entiende.\n’]
Método/atributo Descripción
read([size]) Devuelve datos del archivo como bytes o como cadena de texto
dependiendo del modo de archivo, indicando el argumento opcional size
el número de bytes o caracteres de cadena de texto que hay que leer
readlines([size]) Devuelve una lista de líneas del archivo, con el argumento opcional size
encoding La codificación empleada para interpretar los bytes del archivo como
Unicode (normalmente UTF-8)
Bytes y Unicode con archivos
El comportamiento predeterminado para los archivos de Python (ya sean
de lectura o escritura) es el modo de texto, lo cual significa que el objetivo
es trabajar con cadenas de texto Python (por ejemplo, Unicode). Esto
contrasta con el modo binario, que se puede conseguir añadiendo b al modo
del archivo. Recuperando el archivo de la sección anterior (que contiene
caracteres no ASCII con codificación UTF-8), tenemos:
In [259]: chars
Out[259]: ‘Sueña el r’
In [260]: len(chars)
Out[260]: 10
In [262]: data
Out[262]: b’Sue\xc3\xb1a el ‘
In [264]: data[:4].decode(“utf-8”)
——————————————————————————————————
UnicodeDecodeError Traceback (most recent call last)
<ipython-input-264-846a5c2fed34> in <module>
——> 1 data[:4].decode(“utf-8”)
UnicodeDecodeError: ‘utf-8’ codec can’t decode byte 0xc3 in
position 3: unexpected
end of data
In [270]: f.read(5)
Out[270]: ‘Sueña’
In [271]: f.seek(4)
Out[271]: 4
In [272]: f.read(1)
——————————————————————————————————
UnicodeDecodeError Traceback (most recent call last)
<ipython-input-272-5a354f952aa4> in <module>
——> 1 f.read(1)
/miniconda/envs/book-env/lib/python3.10/codecs.py in
decode(self, input, final)
# decodifica la entrada (teniendo en
320
cuenta el búfer)
321 data = self.buffer + input
—> (result, consumed) =
322 self._buffer_decode(data, self.errors,
final
)
# mantiene sin decodificar la entrada
323
hasta la siguiente llamada
324 self.buffer = data[consumed:]
UnicodeDecodeError: ‘utf-8’ codec can’t decode byte 0xb1 in
position 0: invalid start byte
In [273]: f.close()
3.4 Conclusión
Ahora que ya tenemos parte de los fundamentos del entorno y lenguaje
Python bajo control, ha llegado la hora de avanzar y aprender NumPy y la
computación orientada a arrays en Python.
Capítulo 4
Fundamentos de NumPy: arrays y computación
vectorizada
El cálculo orientado a arrays en Python tiene su origen en 1995, cuando Jim Hugunin creó la
librería Numeric. En los siguientes diez años, muchas comunidades de programación científica
empezaron a hacer programación de arrays en Python, pero el ecosistema de las librerías había
empezado a fragmentarse a principios de los años 2000. En 2005, Travis Oliphant logró crear el
proyecto NumPy a partir de los proyectos Numeric y Numarray de entonces, para ofrecer a la
comunidad un marco único de cálculo de arrays.
Una de las razones por las que NumPy es tan importante para los
cálculos numéricos en Python es porque ha sido diseñado para ser eficiente
con grandes arrays de datos. Existen varias razones para esto:
In [14]: data
Out[14]:
In [15]: data * 10
Out[15]:
array([[ 15., -1., 30.],
[ 0., -30., 65.]])
In [16]: data + data
Out[16]:
array([[ 3. , -0.2, 6. ],
[ 0. , -6. , 13. ]])
In [18]: data.dtype
Out[18]: dtype(‘float64’)
Siempre que vea «array», «array NumPy» o «ndarray» en el texto del libro, en la mayoría
de los casos el término se está refiriendo al objeto ndarray.
Creando ndarrays
La forma más sencilla de crear un array es mediante la función array.
Esta función acepta cualquier objeto similar a una secuencia (incluyendo
otros arrays) y produce un nuevo array NumPy conteniendo los datos
pasados. Por ejemplo, una lista es una buena candidata para la conversión:
In [19]: data1 = [6, 7.5, 8, 0, 1]
In [24]: arr2
Out[24]:
array([[ 1, 2, 3, 4],
[ 5, 6, 7, 8]])
Como data2 era una lista de listas, el array NumPy arr2 tiene dos
dimensiones, con la forma inferida a partir de los datos. Podemos confirmar
esto inspeccionando los atributos ndim y shape:
In [25]: arr2.ndim
Out[25]: 2
In [26]: arr2.shape
Out[26]: (2, 4)
In [28]: arr2.dtype
Out[28]: dtype(‘int64’)
In [29]: np.zeros(10)
Out[29]: array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
In [30]: np.zeros((3, 6))
Out[30]:
array([[0.,0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.]])
In [31]: np.empty((2, 3, 2))
Out[31]:
array([[[0., 0.],
[0., 0.],
[0., 0.]],
[[0., 0.],
[0., 0.],
[0., 0.]]])
No es seguro suponer que numpy.empty devolverá un array de todo ceros. Esta función
devuelve memoria no inicializada y por lo tanto puede contener valores «basura» que no son
cero. Solo se debería utilizar esta función si la intención es poblar el nuevo array con datos.
array Convierte datos de entrada (lista, tupla, array u otro tipo de secuencia) en un ndarray
o bien deduciendo un tipo de datos o especificándolo de forma explícita; copia los
datos de entrada por omisión.
arange Igual que la función range interna, pero devuelve un ndarray en lugar de una lista.
ones, Produce un array de todo unos con la forma y el tipo de datos dados; ones_like
ones_like toma otro array y produce un array ones con la misma forma y tipo de datos.
empty, Crea nuevos arrays asignando nueva memoria, pero no los llena con valores como
empty_like ones y zeros.
full, Produce un array con la forma y el tipo de datos dados y con todos los valores
full_like fijados en el «valor de relleno» indicado; full_like toma otro array y produce un
array rellenado con la misma forma y tipo de datos.
eye, Crea una matriz de identidad cuadrada N × N (con unos en la diagonal y ceros en el
identity resto).
In [35]: arr1.dtype
Out[35]: dtype(‘float64’)
In [36]: arr2.dtype
Out[36]: dtype(‘int32’)
Los tipos de datos son una fuente de flexibilidad de NumPy para
interactuar con datos procedentes de otros sistemas. En la mayoría de los
casos ofrecen un mapeado directamente sobre la representación subyacente
de un disco o memoria, lo que hace posible leer y escribir flujos de datos
binarios en disco y conectar con código escrito en un lenguaje de bajo nivel
como C o FORTRAN. Los tipos de datos numéricos se denominan de la
misma manera: un nombre de tipo, como float o int, seguido de un
número que indica el número de bits por elemento. Un valor estándar de
punto flotante y doble precisión (que se ha usado internamente en el objeto
float de Python) requiere hasta 8 bytes o 64 bits. Así, este tipo se conoce
en NumPy como float64. Véase en la tabla 4.2 el listado completo de los
tipos de datos soportados por NumPy.
No hay que preocuparse por memorizar los tipos de datos de NumPy, especialmente en el caso
de los nuevos usuarios. A menudo solo basta con tener en cuenta el tipo de datos general que se
está manejando, ya sea punto flotante, complejo, entero, booleano, cadena de texto o un objeto
general de Python. Cuando sea necesario tener más control sobre el modo en que los datos se
almacenan en memoria y en disco, especialmente con grandes conjuntos de datos, es bueno
saber que se tiene control sobre el tipo de almacenamiento.
int8, uint8 i1, u1 Tipos enteros con y sin signo de 8 bits (1 byte).
complex64, c8, c16, Números complejos representados por dos floats de 32, 64 y 120,
complex128, c32 respectivamente.
complex256
object 0 Tipo de objeto Python; un valor puede ser cualquier objeto Python.
string_ S Tipo de cadena de texto ASCII de longitud fija (1 byte por carácter);
por ejemplo, para crear un tipo de datos de cadena de texto con
longitud 10, utilizamos "S10".
Existen tipos enteros con y sin signo, y quizá muchos lectores no estén familizarizados con esta
terminología. Un entero con signo puede representar enteros positivos y negativos, mientras que
un entero sin signo solo puede representar enteros que no sean cero. Por ejemplo, int8 (entero
con signo de 8 bits) puede representar enteros de -128 a 127 (inclusive), mientras que uint8
(entero sin signo de 8 bits) puede representar de 0 a 255.
In [38]: arr.dtype
Out[38]: dtype(‘int64’)
In [40]: float_arr
Out[40]: array([1., 2., 3., 4., 5.])
In [41]: float_arr.dtype
Out[41]: dtype(‘float64’)
In [43]: arr
Out[43]: array([ 3.7, -1.2, -2.6, 0.5, 12.9, 10.1])
In [44]: arr.astype(np.int32)
Out[44]: array([ 3, -1, -2, 0, 12, 10], dtype=int32)
In [46]: numeric_strings.astype(float)
Out[46]: array([ 1.25, -9.6 , 42. ])
Conviene tener cuidado al utilizar el tipo numpy.string_, ya que los datos de cadena de texto
de NumPy tienen tamaño fijo y la entrada puede quedar truncada sin previo aviso. pandas
tiene un comportamiento más intuitivo con datos no numéricos.
Si la conversión fallara por alguna razón (como, por ejemplo, que una
cadena de texto no se pudiera convertir a float64), se produciría un
ValueError. Antes solía ser algo perezoso y escribía float en lugar de
np.float64; NumPy asigna a los tipos de Python sus propios tipos de datos
equivalentes.
También se puede emplear el atributo dtype de otro array:
In [47]: int_array = np.arange(10)
In [49]: int_array.astype(calibers.dtype)
Out[49]: array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
In [51]: zeros_uint32
Out[51]: array([0, 0, 0, 0, 0, 0, 0, 0], dtype=uint32)
Llamar a astype crea siempre un nuevo array (una copia de los datos), incluso aunque el
nuevo tipo de datos sea el mismo que el antiguo.
In [53]: arr
Out[53]:
array([[1., 2., 3.],
[4., 5., 6.]])
In [56]: 1 / arr
Out[56]:
array([[1. , 0.5 , 0.3333],
[0.25 , 0.2 , 0.1667]])
In [57]: arr ** 2
Out[57]:
array([[ 1., 4., 9.],
[16., 25., 36.]])
In [59]: arr2
Out[59]:
array([[ 0., 4., 1.],
[ 7., 2., 12.]])
In [62]: arr
Out[62]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [63]: arr[5]
Out[63]: 5
In [64]: arr[5:8]
Out[64]: array([5, 6, 7])
In [65]: arr[5:8] = 12
In [66]: arr
Out[66]: array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])
Una importante primera distinción en las listas internas de Python es que los cortes de array
son vistas del array original, lo que significa que los datos no se copian, y que cualquier
modificación de la vista se verá reflejada en el array de origen.
In [68]: arr_slice
Out[68]: array([12, 12, 12])
In [70]: arr
Out[70]:
In [72]: arr
Out[72]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
Si nos interesa más una copia de un corte de un ndarray que una vista, tendremos que copiar
explícitamente el array (por ejemplo, arr[5:8].copy()). Como veremos más tarde, pandas
funciona también de este modo.
In [74]: arr2d[2]
Out[74]: array([7, 8, 9])
In [76]: arr2d[0, 2]
Out[76]: 3
In [78]: arr3d
Out[78]:
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
arr3d[0] es un array de 2 × 3:
In [79]: arr3d[0]
Out[79]:
array([[1, 2, 3],
[4, 5, 6]])
In [82]: arr3d
Out[82]:
array([[[42, 42, 42],
[42, 42, 42]],
[[ 7, 8, 9],
[10, 11, 12]]])
In [84]: arr3d
Out[84]:
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
In [87]: x
Out[87]:
array([[ 7, 8, 9],
[10, 11, 12]])
In [88]: x[0]
Out[88]: array([7, 8, 9])
Hay que tener en cuenta que, en todos estos casos en los que se han
seleccionado subsecciones del array, los arrays devueltos son vistas.
Esta sintaxis de indexado multidimensional para arrays NumPy no funcionará con
objetos Python normales, como por ejemplo listas de listas.
Al igual que los objetos unidimensionales como las listas de Python, los
ndarrays se pueden cortar con la sintaxis habitual:
In [89]: arr
Out[89]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
In [90]: arr[1:6]
Out[90]: array([ 1, 2, 3, 4, 64])
In [91]: arr2d
Out[91]:
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
In [92]: arr2d[:2]
Out[92]:
array([[1, 2, 3],
[4, 5, 6]])
Como se puede ver, se ha cortado a lo largo del eje 0, el primer eje. Por
lo tanto, un corte selecciona un rango de elementos a lo largo de un eje.
Puede resultar útil leer la expresión arr2d[:2] como «seleccionar las
primeras dos filas de arr2d».
Se pueden pasar varios cortes igual que se pasan varios índices:
Al realizar así los cortes, siempre se obtienen vistas de array del mismo
número de dimensiones. Mezclando índices enteros y cortes, se obtienen
cortes de menos dimensiones.
Por ejemplo, podemos seleccionar la segunda fila pero solo las primeras
dos columnas, de esta forma:
In [94]: lower_dim_slice = arr2d[1, :2]
In [95]: lower_dim_slice.shape
Out[95]: (2,)
De forma similar, es posible elegir la tercera columna pero solo las dos
primeras filas, de este modo:
In [96]: arr2d[:2, 2]
Out[96]: array([3, 6])
Indexado booleano
Veamos un ejemplo en el que tenemos datos en un array y un array de
nombres con duplicados:
In [102]: names
Out[102]: array([‘Bob’, ‘Joe’, ‘Will’, ‘Bob’, ‘Will’, ‘Joe’,
‘Joe’], dtype=’<U4’)
In [103]: data
Out[103]:
array([[ 4, 7],
[ 0, 2],
[ -5, 6],
[ 0, 0],
[ 1, 2],
[-12, -4],
[ 3, 4]])
Supongamos que cada nombre corresponde a una fila del array de datos
y que queremos seleccionar todas las filas con el nombre “Bob”. Como las
operaciones aritméticas, las comparaciones con arrays (como por ejemplo
==) también son vectorizadas. Así, comparar nombres con la cadena de
texto “Bob” produce un array booleano:
In [104]: names == “Bob”
Out[104]: array([ True, False, False, True, False, False,
False])
El array booleano debe tener la misma longitud que el eje del array que
está indexando. Incluso se pueden mezclar y combinar arrays booleanos con
cortes o enteros (o secuencias de enteros; veremos más después en este
mismo capítulo).
En estos ejemplos, seleccionamos de las filas en las que names == “Bob”
e indexamos también las columnas:
In [112]: data[~cond]
Out[112]:
array([[ 0, 2],
[ -5, 6],
[ 1, 2],
[-12, -4],
[ 3, 4]])
Para elegir dos de los tres nombres y combinar así varias condiciones
booleanas, utilizamos operadores aritméticos booleanos como & (and) y |
(or):
In [113]: mask = (names == “Bob”) | (names == “Will”)
In [114]: mask
Out[114]: array([ True, False, True, True, True, False,
False])
In [115]: data[mask]
Out[115]:
array([[ 4, 7],
[-5, 6],
[ 0, 0],
[ 1, 2]])
In [119]: data
Out[119]:
array([[7, 7],
[0, 2],
[7, 7],
[7, 7],
[7, 7],
[0, 0],
[3, 4]])
Indexado so sticado
El indexado sofisticado es un término adoptado por NumPy para
describir el indexado utilizando arrays enteros. Supongamos que tenemos
un array de 8 × 4:
In [120]: arr = np.zeros((8, 4))
In [126]: arr
Out[126]:
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23],
[24, 25, 26, 27],
[28, 29, 30, 31]])
In [131]: arr
Out[131]:
array([[ 0, 1, 2, 3],
[ 0, 5, 6, 7],
[ 8, 9, 0, 11],
[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 0],
[24, 25, 26, 27],
[28, 0, 30, 31]])
In [133]: arr
Out[133]:
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
In [134]: arr.T
Out[134]:
array([[ 0, 5, 10],
[ 1, 6, 11],
[ 2, 7, 12],
[ 3, 8, 13],
[ 4, 9, 14]])
In [139]: arr
Out[139]:
array([[ 0, 1, 0],
[ 1, 2, -2],
[ 6, 3, 2],
[-1, 0, -1],
[ 1, 0, 1]])
In [140]: arr.swapaxes(0, 1)
Out[140]:
array([[ 0, 1, 6, -1, 1],
[ 1, 2, 3, 0, 0],
[ 0, -2, 2, -1, 1]])
swapaxes devuelve de forma parecida una vista de los datos sin hacer
una copia.
In [142]: samples
Out[142]:
array([[-0.2047, 0.4789, -0.5194, -0.5557],
[ 1.9658, 1.3934, 0.0929, 0.2817],
[ 0.769, 1.2464, 1.0072, -1.2962],
[ 0.275, 0.2289, 1.3529, 0.8864]])
In [144]: N = 1_000_000
Método Descripción
standard_normal Saca muestras a partir de una distribución normal con media 0 y desviación
estándar 1.
In [151]: arr
Out[151]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [152]: np.sqrt(arr)
Out[152]:
array([0. , 1. , 1.4142, 1.7321, 2. , 2.2361, 2.4495,
2.6458, 2.8284, 3. ])
In [153]: np.exp(arr)
Out[153]:
array([ 1. , 2.7183, 7.3891, 2 0.0855, 54.5982,
148.4132,
403.4288, 1096.6332, 2980.958 , 8103.0839])
Estas funciones se denominan ufuncs unarias. Otras, como numpy.add o
numpy.maximum, toman dos arrays (de ahí que se llamen ufuncs binarias) y
devuelven un array sencillo como resultado:
In [154]: x = rng.standard_normal(8)
In [155]: y = rng.standard_normal(8)
In [156]: x
Out[156]:
array([-1.3678, 0.6489, 0.3611, -1.9529, 2.3474, 0.9685,
-0.7594, 0.9022])
In [157]: y
Out[157]:
array([-0.467 , -0.0607, 0.7888, -1.2567, 0.5759, 1.399 ,
1.3223, -0.2997])
In [158]: np.maximum(x, y)
Out[158]:
In [160]: arr
Out[160]: array([ 4.5146, -8.1079, -0.7909, 2.2474, -6.718 ,
-0.4084, 8.6237])
In [162]: remainder
Out[162]: array([ 0.5146, -0.1079, -0.7909, 0.2474, -0.718 ,
-0.4084, 0.6237])
In [163]: whole_part
Out[163]: array([ 4., -8., -0., 2., -6., -0., 8.])
Las ufuncs aceptan un argumento opcional out, que les permite asignar
sus resultados a un array existente en lugar de crear uno nuevo:
In [164]: arr
Out[164]: array([ 4.5146, -8.1079, -0.7909, 2.2474, -6.718 ,
-0.4084, 8.6237])
In [166]: np.add(arr, 1)
Out[166]: array([ 5.5146, -7.1079, 0.2091, 3.2474, -5.718 ,
0.5916, 9.6237])
In [168]: out
Out[168]: array([ 5.5146, -7.1079, 0.2091, 3.2474, -5.718 ,
0.5916, 9.6237])
Función Descripción
log, log10, log2, log1p Logaritmo natural (base e), logaritmo en base 10, logaritmo en
base 2 y log(1 + x), respectivamente.
cos, cosh, sin, sin, tan, tanh Funciones trigonométricas normales e hiperbólicas.
Función Descripción
power Eleva los elementos del primer array a las potencias indicadas en
el segundo.
In [171]: ys
Out[171]:
array([[-5. , -5. , -5. , ..., -5. , -5. , -5. ],
[-4.99, -4.99, -4.99, ..., -4.99, -4.99, -4.99],
[-4.98, -4.98, -4.98, ..., -4.98, -4.98, -4.98],
...,
[ 4.97, 4.97, 4.97, ..., 4.97, 4.97, 4.97],
[ 4.98, 4.98, 4.98, ..., 4.98, 4.98, 4.98],
[ 4.99, 4.99, 4.99, ..., 4.99, 4.99, 4.99]])
In [173]: z
Out[173]:
array([[7.0711, 7.064 , 7.0569, ..., 7.0499, 7.0569, 7.064
],
[7.064 , 7.0569, 7.0499, ..., 7.0428, 7.0499, 7.0569],
[7.0569, 7.0499, 7.0428, ..., 7.0357, 7.0428, 7.0499],
...,
[7.0499, 7.0428, 7.0357, ..., 7.0286, 7.0357, 7.0428],
[7.0569, 7.0499, 7.0428, ..., 7.0357, 7.0428, 7.0499],
[7.064 , 7.0569, 7.0499, ..., 7.0428, 7.0499, 7.0569]])
In [176]: plt.colorbar()
Out[176]: <matplotlib.colorbar.Colorbar at 0x7f6253e43ee0>
In [177]: plt.title(“Image plot of $\sqrt{x^2 + y^2}$ for a
grid of values”)
Out[177]: Text(0.5, 1.0, ‘Image plot of $\\sqrt{x^2 + y^2}$
for a grid of values’
In [184]: result
Out[184]: [1.1, 2.2, 1.3, 1.4, 2.5]
Esto tiene varios problemas. Primero, no será muy rápido para arrays
grandes (porque todo el trabajo se está realizando en el código interpretado
de Python). Segundo, no funcionará con arrays multidimensionales. Con
numpy.where se puede hacer esto con una sencilla llamada a una función:
In [186]: result
Out[186]: array([1.1, 2.2, 1.3, 1.4, 2.5])
In [188]: arr
Out[188]:
array([[ 2.6182, 0.7774, 0.8286, -0.959 ],
[-1.2094, -1.4123, 0.5415, 0.7519],
[-0.6588, -1.2287, 0.2576, 0.3129],
[-0.1308, 1.27 , -0.093 , -0.0662]])
In [193]: arr
Out[193]:
array([[-1.1082, 0.136 , 1.3471, 0.0611],
[ 0.0709, 0.4337, 0.2775, 0.5303],
[ 0.5367, 0.6184, -0.795 , 0.3 ],
[-1.6027, 0.2668, -1.2616, -0.0713],
[ 0.474 , -0.4149, 0.0977, -1.6404]])
In [194]: arr.mean()
Out[194]: -0.08719744457434529
In [195]: np.mean(arr)
Out[195]: -0.08719744457434529
In [196]: arr.sum()
Out[196]: -1.743948891486906
In [198]: arr.sum(axis=0)
Out[198]: array([-1.6292, 1.0399, -0.3344, -0.8203])
En este caso arr.mean(axis=1) significa «calcula la media a lo largo de
las columnas», mientras que arr.sum(axis=0) significa «calcula la suma a
lo largo de las filas».
Otros métodos, como cumsum y cumprod, no agregan; lo que hacen es
producir un array de los resultados intermedios:
In [199]: arr = np.array([0, 1, 2, 3, 4, 5, 6, 7])
In [200]: arr.cumsum()
Out[200]: array([ 0, 1, 3, 6, 10, 15, 21, 28])
In [202]: arr
Out[202]:
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
In [203]: arr.cumsum(axis=0)
Out[203]:
array([[ 0, 1, 2],
[ 3, 5, 7],
[ 9, 12, 15]])
In [204]: arr.cumsum(axis=1)
Out[204]:
array([[ 0, 1, 3],
[ 3, 7, 12],
[ 6, 13, 21]])
Consulte en la tabla 4.6 un listado completo. Veremos muchos ejemplos
de estos métodos en acción en los capítulos posteriores.
Método Descripción
sum Suma de todos los elementos del array o a lo largo de un eje; los arrays de longitud
cero tienen suma 0.
In [209]: bools.any()
Out[209]: True
In [210]: bools.all()
Out[210]: False
Ordenación
Al igual que el tipo de lista interno de Python, los arrays NumPy pueden
ordenarse en el momento con el método sort:
In [211]: arr = rng.standard_normal(6)
In [212]: arr
Out[212]: array([ 0.0773, -0.6839, -0.7208, 1.1206, -0.0548,
-0.0824])
In [213]: arr.sort()
In [214]: arr
Out[214]: array([-0.7208, -0.6839, -0.0824, -0.0548, 0.0773,
1.1206])
In [216]: arr
Out[216]:
array([[ 0.936 , 1.2385, 1.2728],
[ 0.4059, -0.0503, 0.2893],
[ 0.1793, 1.3975, 0.292 ],
[ 0.6384, -0.0279, 1.3711],
[-2.0528, 0.3805, 0.7554]])
In [218]: arr
Out[218]:
array([[-2.0528, -0.0503, 0.2893],
[ 0.1793, -0.0279, 0.292 ],
[ 0.4059, 0.3805, 0.7554],
[ 0.6384, 1.2385, 1.2728],
[ 0.936 , 1.3975, 1.3711]])
In [219]: arr.sort(axis=1)
In [220]: arr
Out[220]:
array([[-2.0528, -0.0503, 0.2893],
[-0.0279, 0.1793, 0.292 ],
[ 0.3805, 0.4059, 0.7554],
[ 0.6384, 1.2385, 1.2728],
[ 0.936 , 1.3711, 1.3975]])
In [223]: sorted_arr2
Out[223]: array([-10, -3, 0, 1, 5, 7])
In [225]: np.unique(names)
Out[225]: array([‘Bob’, ‘Joe’, ‘Will’], dtype=’<U4’)
In [227]: np.unique(ints)
Out[227]: array([1, 2, 3, 4])
Método Descripción
in1d(x, y) Calcula un array booleano que indica si cada elemento de x está contenido en y.
setxor1d(x, y) Establece las diferencias simétricas; elementos que están en alguno de los
arrays, pero no en los dos.
In [236]: arch[“b”]
Out[236]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [237]: np.savez_compressed(“arrays_compressed.npz”,
a=arr, b=arr)
In [243]: x
Out[243]:
array([[1., 2., 3.],
[4., 5., 6.]])
In [244]: y
Out[244]:
array([[ 6., 23.],
[-1., 7.],
[ 8., 9.]])
In [245]: x.dot(y)
Out[245]:
array([[ 28., 64.],
[ 67., 181.]])
In [246]: np.dot(x, y)
Out[246]:
array([[ 28., 64.],
[ 67., 181.]])
In [251]: inv(mat)
Out[251]:
array([[ 3.4993, 2.8444, 3.5956, -16.5538, 4.4733],
[ 2.8444, 2.5667, 2.9002, -13.5774, 3.7678],
[ 3.5956, 2.9002, 4.4823, -18.3453, 4.7066],
[-16.5538, -13.5774, -18.3453, 84.0102, -22.0484],
[ 4.4733, 3.7678, 4.7066, -22.0484, 6.0525]])
Función Descripción
eig Calcula los valores propios y vectores propios de una matriz cuadrada.
#! blockstart
import random
position = 0
walk = [position]
nsteps = 1000
for _ in range(nsteps):
step = 1 if random.randint(0, 1) else -1
position += step
walk.append(position)
#! blockend
In [262]: walk.max()
Out[262]: 50
Una estadística más compleja es el tiempo de primer cruce, el paso en el
que el camino aleatorio alcanza un determinado valor. Aquí queremos saber
cuánto tardará el camino aleatorio en llegar al menos 10 pasos más allá del
0 original en cualquier dirección. np.abs(walk) >= 10 nos da un array
booleano indicando el punto en el que el camino ha llegado a 0 o lo ha
superado, pero queremos el índice de los primeros 10 o –10. Resulta que
podemos calcular esto utilizando argmax, que devuelve el primer índice del
valor máximo del array booleano (True es el valor máximo):
In [263]: (np.abs(walk) >= 10).argmax()
Out[263]: 155
Hay que tener en cuenta que usar aquí argmax no es siempre eficaz,
porque en todas las ocasiones realiza una exploración completa del array.
En este caso especial, una vez que se observa un True, sabemos que es el
valor máximo.
In [269]: walks
Out[269]:
array([[ 1, 2, 3, ..., 22, 23, 22],
[ 1, 0, -1, ..., -50, -49, -48],
[ 1, 2, 3, ..., 50, 49, 48],
...,
[ -1, -2, -1, ..., -10, -9, -10],
[ -1, -2, -3, ..., 8, 9, 8],
[ -1, 0, 1, ..., -4, -3, -2]])
In [271]: walks.min()
Out[271]: -120
In [273]: hits30
Out[273]: array([False, True, True, ..., True, False, True])
Podemos emplear este array booleano para elegir las filas de caminos
que realmente cruzan el nivel absoluto 30, y podemos llamar a argmax a lo
largo del eje 1 para obtener los tiempos de cruce:
In [275]: crossing_times = (np.abs(walks[hits30]) >=
30).argmax(axis=1)
In [276]: crossing_times
Out[276]: array([201, 491, 283, ..., 219, 259, 541])
Experimente como desee con otras distribuciones para los pasos que no
sean lanzamientos de monedas de igual tamaño. Únicamente será necesario
utilizar un método de generación aleatoria diferente, como
standard_normal, para generar pasos normalmente distribuidos con una
cierta media y desviación estándar:
In [278]: draws = 0.25 * rng.standard_normal((nwalks,
nsteps))
Recuerde que este enfoque vectorizado requiere la creación de un array con nwalks * nsteps
elementos, que pueden usar una cantidad de memoria grande para simulaciones grandes. Si la
memoria es limitada, será necesario emplear un enfoque distinto.
4.8 Conclusión
Aunque buena parte del resto del libro se centrará en la creación de
habilidades de manipulación de datos con pandas, seguiremos trabajando en
un estilo similar basado en arrays. En el apéndice A, profundizaremos más
en las funciones NumPy para que pueda desarrollar aún más sus habilidades
de cálculo de arrays.
Capítulo 5
Empezar a trabajar con pandas
Mucha gente no sabe que llevo desde 2013 sin estar implicado activamente en el desarrollo
continuado de pandas; desde entonces ha sido un proyecto totalmente gestionado por su
comunidad. No deje de agradecer su gran trabajo a sus desarrolladores y colaboradores.
Durante el resto del libro, voy a emplear los siguientes convenios de
importación para NumPy y pandas:
In [1]: import numpy as np
Así, siempre que vea pd. en el código, sabrá que se estará refiriendo a
pandas. Quizá también le resulte sencillo importar objetos Series y
DataFrame en el espacio de nombres local, dado que se utilizan con tanta
frecuencia:
In [3]: from pandas import Series, DataFrame
Series
Una serie es un objeto unidimensional de estilo array, que contiene una
secuencia de valores (de tipos parecidos a los de NumPy) del mismo tipo y
un array asociado de etiquetas de datos, que corresponde a su índice. El
objeto Series más sencillo está formado únicamente por un array de datos:
In [14]: obj = pd.Series([4, 7, -5, 3])
In [15]: obj
Out[15]:
0 4
1 7
2 -5
3 3
dtype: int64
La representación como cadena de texto de una serie, visualizada
interactivamente, muestra el índice a la izquierda y los valores a la derecha.
Como no especificamos un índice para los datos, se crea uno
predeterminado, formado por los enteros 0 a N – 1 (donde N es la longitud de
los datos). Se puede obtener la representación en array y el objeto índice de
la serie mediante sus atributos array e index, respectivamente:
In [16]: obj.array
Out[16]:
<PandasArray>
[4, 7, -5, 3]
Length: 4, dtype: int64
In [17]: obj.index
Out[17]: RangeIndex(start=0, stop=4, step=1)
In [19]: obj2
Out[19]:
d 4
b 7
a -5
c 3
dtype: int64
In [20]: obj2.index
Out[20]: Index([‘d’, ‘b’, ‘a’, ‘c’], dtype=’object’)
In [22]: obj2[“d”] = 6
Aquí, [“c”, “a”, “d”] se interpreta como una lista de índices, aunque
contenga cadenas de texto en lugar de enteros.
Utilizar funciones NumPy u operaciones de estilo NumPy, como el
filtrado con un array booleano, la multiplicación de escalares o la aplicación
de funciones matemáticas, permitirá conservar el vínculo índice-valor:
In [25]: obj2 * 2
Out[25]:
d 12
b 14
a -10
c 6
dtype: int64
d 403.428793
b 1096.633158
a 0.006738
c 20.085537
dtype: float64
Otra forma de pensar en una serie es como si fuera un diccionario
ordenado de longitud fija, dado que es una asignación de valores de índice a
valores de datos. Se puede emplear en muchos contextos en los que se
podría usar un diccionario:
In [28]: “b” in obj2
Out[28]: True
Ohio 35000
Texas 71000
Oregon 16000
Utah 5000
dtype: int64
In [33]: obj3.to_dict()
Out[33]: {‘Ohio’: 35000, ‘Texas’: 71000, ‘Oregon’: 16000,
‘Utah’: 5000}
In [36]: obj4
Out[36]:
California NaN
Ohio 35000.0
Oregon 16000.0
Texas 71000.0
dtype: float64
In [37]: pd.isna(obj4)
Out[37]:
California True
Ohio False
Oregon False
Texas False
dtype: bool
In [38]: pd.notna(obj4)
Out[38]:
California False
Ohio True
Oregon True
Texas True
dtype: bool
In [39]: obj4.isna()
Out[39]:
California True
Ohio False
Oregon False
Texas False
dtype: bool
Hablaré con más detalle del trabajo con datos ausentes en el capítulo 7.
Una característica útil del objeto Series para muchas aplicaciones es que en
las operaciones aritméticas se alinea automáticamente por etiqueta de
índice:
In [40]: obj3
Out[40]:
Ohio 35000
Texas 71000
Oregon 16000
Utah 5000
dtype: int64
In [41]: obj4
Out[41]:
California NaN
Ohio 35000.0
Oregon 16000.0
Texas 71000.0
dtype: float64
In [45]: obj4
Out[45]:
state
California NaN
Ohio 35000.0
Oregon 16000.0
Texas 71000.0
Name: population, dtype: float64
In [46]: obj
Out[46]:
0 4
1 7
2 -5
3 3
dtype: int64
DataFrame
Un dataframe representa una tabla rectangular de datos, y contiene una
colección de columnas ordenada y con nombre, cada una de las cuales
puede tener un tipo de valor distinto (numérico, cadena de texto, booleano,
etc.). El objeto DataFrame tiene un índice de fila y otro de columna; se
podría considerar como un diccionario de objetos Series que comparten
todos el mismo índice.
Aunque un dataframe tiene físicamente dos dimensiones, se puede utilizar para representar
datos de más dimensiones en un formato tabular, empleando la indexación jerárquica, un tema
del que hablaremos en el capítulo 8, y un ingrediente de algunas de las características de
manejo de datos más avanzadas de pandas.
Si está utilizando Jupyter notebook, los objetos DataFrame de pandas se mostrarán como
una tabla HTML apta para navegadores. Véase un ejemplo en la figura 5.1.
In [56]: frame2.columns
Out[56]: Index([‘year’, ‘state’, ‘pop’, ‘debt’],
dtype=’object’)
In [57]: frame2[“state”]
Out[57]:
0 Ohio
1 Ohio
2 Ohio
3 Nevada
4 Nevada
5 Nevada
Name: state, dtype: object
In [58]: frame2.year
Out[58]:
0 2000
1 2001
2 2002
3 2001
4 2002
5 2003
Name: year, dtype: int64
El acceso de estilo atributo (por ejemplo, frame2.year) y el completado por tabulación de los
nombres de columna en IPython se incluyen por comodidad. frame2[column] sirve con
cualquier nombre de columna, pero frame2.column solo funciona cuando el nombre de la
columna es un nombre de variable válido de Python, y no entra en conflicto con ninguno de los
nombres de métodos del objeto DataFrame. Por ejemplo, si el nombre de una columna contiene
espacios en blanco o símbolos distintos al carácter de subrayado, no se puede acceder a ella con
el método de atributo de punto.
In [59]: frame2.loc[1]
Out[59]:
year 2001
state Ohio
pop 1.7
debt NaN
Name: 1, dtype: object
In [60]: frame2.iloc[2]
Out[60]:
year 2002
state Ohio
pop 3.6
debt NaN
Name: 2, dtype: object
In [62]: frame2
Out[62]:
year state pop debt
0 2000 Ohio 1.5 16.5
1 2001 Ohio 1.7 16.5
2 2002 Ohio 3.6 16.5
3 2001 Nevada 2.4 16.5
4 2002 Nevada 2.9 16.5
5 2003 Nevada 3.2 16.5
In [64]: frame2
Out[64]:
In [67]: frame2
Out[67]:
In [69]: frame2
Out[69]:
In [71]: frame2.columns
Out[71]: Index([‘year’, ‘state’, ‘pop’, ‘debt’],
dtype=’object’)
La columna devuelta al indexar un dataframe es una vista de los datos subyacentes, no una
copia. Por lo tanto, cualquier modificación que se haga en ese momento sobre la serie se verá
reflejada en el dataframe. La columna se puede copiar de forma explícita con el método copy
del objeto Series.
In [74]: frame3
Out[74]:
Ohio Nevada
2000 1.5 NaN
2001 1.7 2.4
2002 3.6 2.9
Conviene tener en cuenta que la transposición descarta los tipos de datos de las columnas si
estas no tienen todas el mismo tipo de datos, de modo que transponerlas y después cancelar la
transposición conseguirá que se pierda la información anterior de tipos. En este caso las
columnas se convierten en arrays de objetos Python puros.
Ohio Nevada
2001 1.7 2.4
2002 3.6 2.9
2003 NaN NaN
In [78]: pd.DataFrame(pdata)
Out[78]:
Ohio Nevada
2000 1.5 NaN
2001 1.7 2.4
Tipo Notas
ndarray de dos Una matriz de datos, que pasa etiquetas de fila y columna opcionales.
dimensiones
Diccionario de Cada valor se convierte en una columna; los índices de cada serie se unen entre
series sí para formar el índice de fila del resultado si no se le pasa un índice explícito.
Tipo Notas
Diccionario de Cada diccionario interno se convierte en una columna; las claves se unen para
diccionarios formar el índice de fila como en el tipo «diccionario de series».
Lista de Cada elemento se convierte en una fila en el dataframe; las uniones de claves
diccionarios o de diccionario o índices de series se convierten en las etiquetas de columna del
series dataframe.
Otro dataframe Se utilizan los índices del dataframe, a menos que se pasen otros distintos.
MaskedArray de Igual que el tipo «ndarray de dos dimensiones», excepto que los valores
NumPy enmascarados faltan en el resultado del dataframe.
In [82]: frame3.to_numpy()
Out[82]:
array([[1.5, nan],
[1.7, 2.4],
[3.6, 2.9]])
Si las columnas del dataframe tienen tipos de datos distintos, se elegirá
el tipo de datos del array devuelto para que acomode todas las columnas:
In [83]: frame2.to_numpy()
Out[83]:
array([[2000, ‘Ohio’, 1.5, nan],
[2001, ‘Ohio’, 1.7, nan],
[2002, ‘Ohio’, 3.6, nan],
[2001, ‘Nevada’, 2.4, nan],
[2002, ‘Nevada’, 2.9, nan],
[2003, ‘Nevada’, 3.2, nan]], dtype=object)
Objetos índice
Los objetos índice de pandas son responsables de albergar etiquetas de
ejes (incluyendo los nombres de columnas de un dataframe) y otros
metadatos (como el nombre o los nombres de los ejes). Cualquier array u
otra secuencia de etiquetas que se utilice al construir una serie o un
dataframe se convierte internamente en un índice:
In [84]: obj = pd.Series(np.arange(3), index=[“a”, “b”,
“c”])
In [86]: index
Out[86]: Index([‘a’, ‘b’, ‘c’], dtype=’object’)
In [87]: index[1:]
Out[87]: Index([‘b’, ‘c’], dtype=’object’)
Los objetos índice son inmutables, y por eso no pueden ser modificados
por el usuario:
index[1] = “d” # TypeError
In [91]: obj2
Out[91]:
0 1.5
1 -2.5
2 0.0
dtype: float64
Algunos usuarios no aprovecharán demasiado las capacidades ofrecidas por un objeto índice,
pero como algunas operaciones producen resultados que contienen datos indexados, es
importante comprender cómo funcionan.
In [94]: frame3.columns
Out[94]: Index([‘Ohio’, ‘Nevada’], dtype=’object’,
name=’state’)
Método/propiedad Descripción
is_monotonic Devuelve True si cada elemento es mayor o igual que el elemento anterior.
Reindexación
Un método importante de los objetos pandas es la reindexación, que
significa crear un nuevo objeto con los valores reordenados para que se
alineen con el nuevo índice. Veamos un ejemplo:
In [98]: obj = pd.Series([4.5, 7.2, -5.3, 3.6], index=[“d”,
“b”, “a”, “c”])
In [99]: obj
Out[99]:
d 4.5
b 7.2
a -5.3
c 3.6
dtype: float64
Llamar a reindex en esta serie reordena los datos según el nuevo índice,
introduciendo los valores faltantes si algunos valores de índice no estaban
ya presentes:
In [100]: obj2 = obj.reindex([“a”, “b”, “c”, “d”, “e”])
In [101]: obj2
Out[101]:
a -5.3
b 7.2
c 3.6
d 4.5
e NaN
dtype: float64
Para series ordenadas, como, por ejemplo, las series temporales, quizá
sea más interesante interpolar o rellenar con valores al reindexar. La opción
method nos permite hacer esto, utilizando un método como ffill, que
rellena hacia delante los valores:
In [102]: obj3 = pd.Series([“blue”, “purple”, “yellow”],
index=[0, 2, 4])
In [103]: obj3
Out[103]:
0 blue
2 purple
4 yellow
dtype: object
0 blue
1 blue
2 purple
3 purple
4 yellow
5 yellow
dtype: object
In [106]: frame
Out[106]:
Ohio Texas California
a 0 1 2
c 3 4 5
d 6 7 8
In [110]: frame.reindex(columns=states)
Out[110]:
Argumento Descripción
labels Nueva secuencia a utilizar como índice. Puede ser una instancia del índice o
cualquier otra estructura de datos Python de tipo secuencia. Un índice se utilizará
exactamente como tal, sin realizar copia alguna.
method Método de interpolación (relleno): "ffill" rellena hacia delante, mientras que
"bfill" rellena hacia atrás.
limit Cuando se rellena hacia delante o hacia atrás, el hueco de tamaño máximo (en
número de elementos) a rellenar.
tolerance Cuando se rellena hacia delante o hacia atrás, el hueco de tamaño máximo (en
distancia numérica absoluta) a rellenar para coincidencias inexactas.
level Coincide con el índice sencillo a nivel del índice jerárquico (MultiIndex); en caso
contrario selecciona un subconjunto.
copy Si es True, copia siempre los datos subyacentes incluso aunque el nuevo índice sea
equivalente al antiguo; si es False, no copia los datos cuando los índices son
equivalentes.
Como veremos posteriormente en el apartado «Selección en dataframes
con loc e iloc», también se puede reindexar utilizando el operador loc, y
muchos usuarios prefieren hacer esto siempre de esta forma. Esto funciona
solamente si todas las etiquetas del nuevo índice ya existen en el dataframe
(mientras que reindex insertará datos faltantes para etiquetas nuevas):
In [112]: frame.loc[[“a”, “d”, “c”], [“California”,
“Texas”]]
Out[112]:
California Texas
a 2 1
d 8 7
c 5 4
In [114]: obj
Out[114]:
a 0.0
b 1.0
c 2.0
d 3.0
e 4.0
dtype: float64
In [116]: new_obj
Out[116]:
a 0.0
b 1.0
d 3.0
e 4.0
dtype: float64
In [119]: data
Out[119]:
one three
Ohio 0 2
Colorado 4 6
Utah 8 10
New York 12 14
Indexación, selección y ltrado
La indexación de series (obj[...]) funciona de manera análoga a la
indexación de arrays NumPy, excepto que se pueden utilizar los valores de
índice de la serie en lugar de solamente enteros. Aquí tenemos algunos
ejemplos:
In [124]: obj = pd.Series(np.arange(4.), index=[“a”, “b”,
“c”, “d”])
In [125]: obj
Out[125]:
a 0.0
b 1.0
c 2.0
d 3.0
dtype: float64
In [126]: obj[“b”]
Out[126]: 1.0
In [127]: obj[1]
Out[127]: 1.0
In [128]: obj[2:4]
Out[128]:
c 2.0
d 3.0
dtype: float64
In [135]: obj1
Out[135]:
2 1
0 2
1 3
dtype: int64
In [136]: obj2
Out[136]:
a 1
b 2
c 3
dtype: int64
También se puede cortar con etiquetas, pero funciona de un modo distinto al corte
normal de Python en que el punto final es inclusivo:
In [141]: obj2.loc[“b”:”c”]
Out[141]:
b 2
c 3
dtype: int64
In [143]: obj2
Out[143]:
a 1
b 5
c 5
dtype: int64
Puede ser un error habitual de principiante intentar llamar a loc o iloc como funciones en
lugar de «indexar dentro de» ellas con corchetes. La notación de corchetes se utiliza para
habilitar las operaciones de corte y permitir la indexación en varios ejes con objetos
DataFrame.
In [145]: data
Out[145]:
In [146]: data[“two”]
Out[146]:
Ohio 1
Colorado 5
Utah 9
New York 13
Name: two, dtype: int64
In [152]: data
Out[152]:
In [154]: data.loc[“Colorado”]
Out[154]:
one 0
two 5
three 6
four 7
Name: Colorado, dtype: int64
El resultado de esto es una serie, con un índice que contiene las etiquetas
de columna del dataframe. Para seleccionar varios roles creando un nuevo
dataframe, pasamos una secuencia de etiquetas:
In [155]: data.loc[[“Colorado”, “New York”]]
Out[155]:
one two three four
Colorado 0 5 6 7
New York 12 13 14 15
In [157]: data.iloc[2]
Out[157]:
one 8
two 9
three 10
four 11
Name: Utah, dtype: int64
Ohio 0
Colorado 5
Utah 9
Name: two, dtype: int64
Los arrays booleanos se pueden utilizar con loc pero no con iloc:
In [163]: data.loc[data.three >= 2]
Out[163]:
Tipo Notas
df[column] Selecciona una sola columna o una secuencia de columnas del dataframe; casos
especiales: array booleano (filtrado de filas), corte (segmentado de filas) o
dataframe booleano (valores de conjunto basados en el mismo criterio).
df.loc[rows] Selecciona una sola fila o un subconjunto de filas del dataframe por etiqueta.
df.loc[rows, Selecciona una fila (o filas) y una columna (o columnas) por etiqueta.
cols]
df.iloc[rows] Selecciona una sola fila o un subconjunto de filas del dataframe por posición de
entero.
df.iloc[rows, Selecciona una fila (o filas) y una columna (o columnas) por posición de entero.
cols]
df.iat[row, Selecciona un solo valor escalar por posición de fila y columna (enteros).
col]
In [165]: ser
Out[165]:
0 0.0
1 1.0
2 2.0
dtype: float64
In [166]: ser[-1]
——————————————————————————————————
ValueError Traceback (most recent call last)
/miniconda/envs/book-env/lib/python3.10/site-
packages/pandas/core/indexes/range.py in get_loc(self, key,
method, tolerance)
384 try:
—
385 return self._range.index(new_key)
>
386 except ValueError as err:
ValueError: -1 is not in range
The above exception was the direct cause of the following
exception:
KeyError Traceback (most recent call last)
<ipython-input-166-44969a759c20> in <module>
——> 1 ser[-1]
/miniconda/envs/book-env/lib/python3.10/site-
packages/pandas/core/series.py in __getitem__(self, key)
956
957 elif key_is_scalar:
—
958 return self._get_value(key)
>
959
960 if is_hashable(key):
/miniconda/envs/book-env/lib/python3.10/site-
packages/pandas/core/series.py in _get_value(self, label,
takeable)
1067
1068 # Similar a Index.get_value, pero no volvemos
a caer en posicional
—
1069 loc = self.index.get_loc(label)
>
return self.index._get_values_for_loc(self,
1070
loc, label)
1071
/miniconda/envs/book-env/lib/python3.10/site-
packages/pandas/core/indexes/range.py in get_loc(self, key,
method, tolerance)
385 return self._range.index(new_key)
386 except ValueError as err:
—
387 raise KeyError(key) from err
>
388 self._check_indexing_error(key)
389 raise KeyError(key)
KeyError: -1
0 0.0
1 1.0
2 2.0
dtype: float64
In [169]: ser2[-1]
Out[169]: 2.0
Si tenemos un índice de eje que contiene enteros, la selección de datos
siempre estará orientada a las etiquetas. Como ya he dicho anteriormente, si
se utiliza loc (para las etiquetas) o iloc (para los enteros), se obtiene
exactamente lo que se desea:
In [170]: ser.iloc[-1]
Out[170]: 2.0
0 0.0
1 1.0
dtype: float64
In [173]: data
Out[173]:
In [174]: data.iloc[2] = 5
In [175]: data
Out[175]:
In [177]: data
Out[177]:
In [181]: data
Out[181]:
In [184]: s1
Out[184]:
a 7.3
c -2.5
d 3.4
e 1.5
dtype: float64
In [185]: s2
Out[185]:
a -2.1
c 3.6
e -1.5
f 4.0
g 3.1
dtype: float64
a 5.2
c 1.1
d NaN
e 0.0
f NaN
g NaN
dtype: float64
La alineación interna de datos introduce valores faltantes en las
ubicaciones de etiquetas que no se superponen. Los valores faltantes se
propagarán entonces en cálculos aritméticos posteriores.
En el caso de los objetos DataFrame, la alineación se realiza en filas y
columnas:
In [189]: df1
Out[189]:
b c d
Ohio 0.0 1.0 2.0
Texas 3.0 4.0 5.0
Colorado 6.0 7.0 8.0
In [190]: df2
Out[190]:
b d e
Utah 0.0 1.0 2.0
Ohio 3.0 4.0 5.0
Texas 6.0 7.0 8.0
Oregon 9.0 10.0 11.0
In [194]: df1
Out[194]:
A
0 1
1 2
In [195]: df2
Out[195]:
B
0 3
1 4
A B
0 NaN NaN
1 NaN NaN
Métodos aritméticos con valores de relleno
In [200]: df1
Out[200]:
a b c d
0 0.0 1.0 2.0 3.0
1 4.0 5.0 6.0 7.0
2 8.0 9.0 10.0 11.0
In [201]: df2
Out[201]:
a b c d e
0 0.0 1.0 2.0 3.0 4.0
1 5.0 NaN 7.0 8.0 9.0
2 10.0 11.0 12.0 13.0 14.0
3 15.0 16.0 17.0 18.0 19.0
a b c d e
0 0.0 2.0 4.0 6.0 NaN
1 9.0 NaN 13.0 15.0 NaN
2 18.0 20.0 22.0 24.0 NaN
3 NaN NaN NaN NaN NaN
a b c d e
0 0.0 2.0 4.0 6.0 4.0
1 9.0 5.0 13.0 15.0 9.0
2 18.0 20.0 22.0 24.0 14.0
3 15.0 16.0 17.0 18.0 19.0
a b c d
0 inf 1.000000 0.500000 0.333333
1 0.250 0.200000 0.166667 0.142857
2 0.125 0.111111 0.100000 0.090909
In [205]: df1.rdiv(1)
Out[205]:
a b c d
0 inf 1.000000 0.500000 0.333333
1 0.250 0.200000 0.166667 0.142857
2 0.125 0.111111 0.100000 0.090909
a b c d e
0 0.0 1.0 2.0 3.0 0
1 4.0 5.0 6.0 7.0 0
2 8.0 9.0 10.0 11.0 0
Método Descripción
In [208]: arr
Out[208]:
array([[ 0., 1., 2., 3.],
In [209]: arr[0]
Out[209]: array([0., 1., 2., 3.])
In [210]: arr–arr[0]
Out[210]:
array([[0., 0., 0., 0.],
Cuando restamos arr[0] de arr, la resta se realiza una vez por cada fila.
A esto se denomina difusión, y se explica con más detalle en el apéndice A,
ya que tiene que ver con los arrays NumPy en general. Las operaciones
entre un dataframe y una serie son similares:
In [211]: frame = pd.DataFrame(np.arange(12.).reshape((4,
3)),
.....: columns=list(“bde”),
.....: index=[“Utah”, “Ohio”, “Texas”, “Oregon”])
In [213]: frame
Out[213]:
b d e
Utah 0.0 1.0 2.0
Ohio 3.0 4.0 5.0
Texas 6.0 7.0 8.0
Oregon 9.0 10.0 11.0
In [214]: series
Out[214]:
b 0.0
d 1.0
e 2.0
Name: Utah, dtype: float64
b d e
Utah 0.0 0.0 0.0
Ohio 3.0 3.0 3.0
Texas 6.0 6.0 6.0
Oregon 9.0 9.0 9.0
In [217]: series2
Out[217]:
b 0
e 1
f 2
dtype: int64
b d e f
Utah 0.0 NaN 3.0 NaN
Ohio 3.0 NaN 6.0 NaN
Texas 6.0 NaN 9.0 NaN
Oregon 9.0 NaN 12.0 NaN
In [220]: frame
Out[220]:
b d e
Utah 0.0 1.0 2.0
Ohio 3.0 4.0 5.0
Texas 6.0 7.0 8.0
Oregon 9.0 10.0 11.0
In [221]: series3
Out[221]:
Utah 1.0
Ohio 4.0
Texas 7.0
Oregon 10.0
Name: d, dtype: float64
b d e
Utah -1.0 0.0 1.0
Ohio -1.0 0.0 1.0
Texas -1.0 0.0 1.0
Oregon -1.0 0.0 1.0
El eje que se pasa es el eje con el que coincidir. En este caso queremos
decir que hay que coincidir con el índice de fila del DataFrame
(axis=”index”) y difundir a lo largo de las columnas.
.....: columns=list(“bde”),
.....: index=[“Utah”, “Ohio”, “Texas”, “Oregon”])
In [224]: frame
Out[224]:
b d e
Utah -0.204708 0.478943 -0.519439
Ohio -0.555730 1.965781 1.393406
Texas 0.092908 0.281746 0.769023
Oregon 1.246435 1.007189 -1.296221
In [225]: np.abs(frame)
Out[225]:
b d e
Utah 0.204708 0.478943 0.519439
Ohio 0.555730 1.965781 1.393406
Texas 0.092908 0.281746 0.769023
Oregon 1.246435 1.007189 1.296221
In [227]: frame.apply(f1)
Out[227]:
b 1.802165
d 1.684034
e 2.689627
dtype: float64
Utah 0.998382
Ohio 2.521511
Texas 0.676115
Oregon 2.542656
dtype: float64
Muchas de las estadísticas de array más comunes (como sum y mean) son
métodos del objeto DataFrame, de modo que no es necesario utilizar apply.
La función pasada a apply no tiene que devolver un valor escalar;
también puede devolver una serie con varios valores:
In [230]: frame.apply(f2)
Out[230]:
b d e
min -0.555730 0.281746 -1.296221
max 1.246435 1.965781 1.393406
In [232]: frame.applymap(my_format)
Out[232]:
b d e
Utah -0.20 0.48 -0.52
Ohio -0.56 1.97 1.39
Texas 0.09 0.28 0.77
Oregon 1.25 1.01 -1.30
La razón del nombre applymap es que las series tienen un método map
para aplicar una función por elementos:
In [233]: frame[“e”].map(my_format)
Out[233]:
Utah -0.52
Ohio 1.39
Texas 0.77
Oregon -1.30
Name: e, dtype: object
In [235]: obj
Out[235]:
d 0
a 1
b 2
c 3
dtype: int64
In [236]: obj.sort_index()
Out[236]:
a 1
b 2
c 3
d 0
dtype: int64
In [238]: frame
Out[238]:
d a b c
three 0 1 2 3
one 4 5 6 7
In [239]: frame.sort_index()
Out[239]:
d a b c
one 4 5 6 7
three 0 1 2 3
In [240]: frame.sort_index(axis=”columns”)
Out[240]:
a b c d
three 1 2 3 0
one 5 6 7 4
d c b a
three 0 3 2 1
one 4 7 6 5
In [243]: obj.sort_values()
Out[243]:
2 -3
3 2
0 4
1 7
dtype: int64
Los valores que puedan faltar se ordenan al final de la serie de forma
predeterminada:
In [244]: obj = pd.Series([4, np.nan, 7, np.nan, -3, 2])
In [245]: obj.sort_values()
Out[245]:
4 -3.0
5 2.0
0 4.0
2 7.0
1 NaN
3 NaN
dtype: float64
1 NaN
3 NaN
4 -3.0
5 2.0
0 4.0
2 7.0
dtype: float64
In [248]: frame
Out[248]:
b a
0 4 0
1 7 1
2 -3 0
3 2 1
In [249]: frame.sort_values(“b”)
Out[249]:
b a
2 -3 0
3 2 1
0 4 0
1 7 1
b a
2 3 0
0 4 0
3 2 1
1 7 1
In [252]: obj.rank()
Out[252]:
0 6.5
1 1.0
2 6.5
3 4.5
4 3.0
5 2.0
6 4.5
dtype: float64
0 6.0
1 1.0
2 7.0
3 4.0
4 3.0
5 2.0
6 5.0
dtype: float64
En este caso, en lugar de usar el rango promedio 6.5 para las entradas 0
y 2, han sido fijadas en 6 y 7, porque la etiqueta 0 precede a la etiqueta 2 en
los datos.
Se puede organizar también en orden descendente:
In [254]: obj.rank(ascending=False)
Out[254]:
0 1.5
1 7.0
2 1.5
3 3.5
4 5.0
5 6.0
6 3.5
dtype: float64
Véase en la tabla 5.6 una lista de métodos de desempate disponibles.
Método Descripción
"average" Valor predeterminado: asigna el rango medio a cada entrada del grupo empatado.
"first" Asigna rangos en el orden en que aparecen los valores en los datos.
"dense" Igual que method="min", pero los rangos siempre aumentan en 1 entre grupos en
lugar del número de elementos iguales en un grupo.
In [256]: frame
Out[256]:
b a c
0 4.3 0 -2.0
1 7.0 1 5.0
2 -3.0 0 8.0
3 2.0 1 -2.5
In [257]: frame.rank(axis=”columns”)
Out[257]:
b a c
0 3.0 2.0 1.0
1 3.0 1.0 2.0
2 1.0 2.0 3.0
3 3.0 2.0 1.0
In [259]: obj
Out[259]:
a 0
a 1
b 2
b 3
c 4
dtype: int64
a 0
a 1
dtype: int64
In [262]: obj[“c”]
Out[262]: 4
In [264]: df
Out[264]:
0 1 2
a 0.274992 0.228913 1.352917
a 0.886429 -2.001637 -0.371843
b 1.669025 -0.438570 -0.539741
b 0.476985 3.248944 -1.021228
c -0.577087 0.124121 0.302614
In [265]: df.loc[“b”]
Out[265]:
0 1 2
b 1.669025 -0.438570 -0.539741
b 0.476985 3.248944 -1.021228
In [266]: df.loc[“c”]
Out[266]:
0 -0.577087
1 0.124121
2 0.302614
Name: c, dtype: float64
5.3 Resumir y calcular estadísticas descriptivas
Los objetos pandas están equipados con un conjunto de métodos
matemáticos y estadísticos comunes. La mayoría entran en la categoría de
reducciones o estadísticas de resumen, es decir, métodos que extraen un
solo valor (como la suma o el promedio) de una serie, o una serie de valores
de las filas y columnas de un dataframe. Comparados con los métodos
similares que se pueden encontrar en los arrays NumPy, incorporan
manipulación de datos faltantes. Veamos este pequeño dataframe:
In [267]: df = pd.DataFrame([[1.4, np.nan], [7.1, -4.5],
In [268]: df
Out[268]:
one two
a 1.40 NaN
b 7.10 -4.5
c NaN NaN
d 0.75 -1.3
Llamar al método sum del dataframe devuelve una serie que contiene
sumas de columna:
In [269]: df.sum()
Out[269]:
one 9.25
two -5.80
dtype: float64
a 1.40
b 2.60
c 0.00
d -0.55
dtype: float64
Cuando todos los valores de una fila o columna son nulos o faltan, la
suma es 0, mientras que, si hay algún valor que no es nulo, entonces el
resultado es no nulo o faltante. Esto se puede deshabilitar con la opción
skipna, en cuyo caso cualquier valor nulo en una fila o columna asigna al
resultado correspondiente el nombre de nulo o faltante:
In [271]: df.sum(axis=”index”, skipna=False)
Out[271]:
one NaN
two NaN
dtype: float64
a NaN
b 2.60
c NaN
d -0.55
dtype: float64
a 1.400
b 1.300
c NaN
d -0.275
dtype: float64
Véase en la tabla 5.7 una lista de opciones habituales para cada método
de reducción.
Método Descripción
axis Eje para reducir; "index" para las filas del dataframe y "columns" para las columnas.
level Reduce los agrupados por nivel si el eje se indexa de forma jerárquica (MultiIndex).
one b
two d
dtype: object
one two
a 1.40 NaN
b 8.50 -4.5
c NaN NaN
d 9.25 -5.8
Algunos métodos no son ni reducciones ni acumulaciones. Un ejemplo
es describe, dado que produce varias estadísticas de resumen de una sola
vez:
In [276]: df.describe()
Out[276]:
one two
count 3.000000 2.000000
mean 3.083333 -2.900000
std 3.493685 2.262742
min 0.750000 -4.500000
25% 1.075000 -3.700000
50% 1.400000 2.900000
75% 4.250000 -2.100000
max 7.100000 -1.300000
count 16
unique 3
top a
freq 8
dtype: object
Método Descripción
Método Descripción
argmin, Calcula ubicaciones de índice (enteros) en las que se obtienen los valores mínimo o
argmax máximo, respectivamente; no está disponible con objetos Dataframe.
idxmin, Calcula etiquetas de índice en las que se obtienen los valores mínimo o máximo,
idxmax respectivamente.
Correlación y covarianza
Algunas estadísticas de resumen, como la correlación y la covarianza, se
calculan a partir de pares de argumentos. Supongamos algunos dataframes
de precios y volúmenes de acciones, obtenidos originalmente de Yahoo!
Finance y disponibles en archivos pickle binarios de Python, que se pueden
encontrar en los conjuntos de datos que acompañan al libro:
In [279]: price = pd.read_pickle(“examples/yahoo_price.pkl”)
In [280]: volume =
pd.read_pickle(“examples/yahoo_volume.pkl”)
In [282]: returns.tail()
Out[282]:
In [284]: returns[“MSFT”].cov(returns[“IBM”])
Out[284]: 8.870655479703546e-05
Los métodos corr y cov del objeto DataFrame, por otro lado, devuelven
una correlación completa o una matriz de covarianza como un dataframe,
respectivamente:
In [286]: returns.corr()
Out[286]:
In [287]: returns.cov()
Out[287]:
AAPL 0.386817
GOOG 0.405099
IBM 1.000000
MSFT 0.499764
dtype: float64
Pasar un dataframe calcula las correlaciones de los nombres de columna
coincidentes. En este caso se han calculado las correlaciones de los cambios
de porcentaje con volumen:
In [289]: returns.corrwith(volume)
Out[289]:
AAPL -0.075565
GOOG -0.007067
IBM -0.204849
MSFT -0.092950
dtype: float64
Sin embargo, pasar axis=”columns” hace las cosas fila a fila. En todos
los casos, los puntos de datos se alinean por etiqueta antes de que se calcule
la correlación.
In [292]: uniques
Out[292]: array([‘c’, ‘a’, ‘d’, ‘b’], dtype=object)
c 3
a 3
b 2
d 1
dtype: int64
c 3
a 3
d 1
b 2
dtype: int64
0 c
1 a
2 d
3 a
4 a
5 b
6 b
7 c
8 c
dtype: object
In [297]: mask
Out[297]:
0 True
1 False
2 False
3 False
4 False
5 True
6 True
7 True
8 True
dtype: bool
In [298]: obj[mask]
Out[298]:
0 c
5 b
6 b
7 c
8 c
dtype: object
In [301]: indices =
pd.Index(unique_vals).get_indexer(to_match)
In [302]: indices
Out[302]: array([0, 2, 1, 1, 0, 2])
Método Descripción
isin Calcula un array booleano indicando si cada valor de una serie o un dataframe
está contenido en la secuencia de valores pasada.
get_indexer Calcula índices enteros para cada valor de un array para obtener otro array de
valores diferentes; es útil para operaciones de alineación de datos y de tipo JOIN.
unique Calcula un array de valores únicos de una serie, devueltos en el orden observado.
value_counts Devuelve una serie que contiene valores únicos como índice y frecuencias como
valores, un recuento ordenado en orden descendente.
In [304]: data
Out[304]:
1 1
3 2
4 2
In [307]: result
Out[307]:
En este caso, las etiquetas de fila del resultado son los distintos valores
que ocurren en todas las columnas. Los valores son los recuentos
respectivos de estos valores en cada columna.
Hay también un metodo DataFrame.value_counts, pero calcula los
recuentos teniendo en cuenta cada fila del dataframe como una tupla, para
determinar el número de apariciones de cada fila diferente:
In [308]: data = pd.DataFrame({“a”: [1, 1, 1, 2, 2], “b”:
[0, 0, 1, 0, 0]})
In [309]: data
Out[309]:
a b
0 1 0
1 1 0
2 1 1
3 2 0
4 2 0
In [310]: data.value_counts()
Out[310]:
a b
1 0 2
2 0 2
1 1 1
dtype: int64
5.4 Conclusión
En el siguiente capítulo, hablaremos de herramientas para leer (o cargar)
y escribir conjuntos de datos con pandas. A continuación, profundizaremos
más en herramientas de limpieza, disputa, análisis y visualización de datos
utilizando pandas.
Capítulo 6
Carga de datos, almacenamiento y formatos de archivo
Leer datos y hacerlos accesibles (lo que se denomina carga de datos) es un primer
paso necesario para utilizar la mayor parte de las herramientas de este libro. El
término «análisis» se emplea también en ocasiones para describir la carga de datos de
texto y su interpretación como tablas y como distintos tipos de datos. Voy a centrarme
en la entrada y salida de datos mediante pandas, aunque hay herramientas en otras
librerías que ayudan con la lectura y escritura de datos en diferentes formatos.
Normalmente, se puede clasificar la entrada y salida de datos en varias categorías
principales: leer archivos de texto y otros formatos en disco más eficientes, cargar
datos de bases de datos e interactuar con fuentes de red, como, por ejemplo, API web.
Función Descripción
read_csv Carga datos delimitados de un archivo, una URL o un objeto de tipo archivo; usa la coma
como delimitador predeterminado.
read_fwf Lee datos en formato de columna de anchura fija (es decir, sin delimitadores).
read_clipboard Variación de read_csv que lee datos del portapapeles; es útil para convertir tablas a partir de
páginas web.
read_json Lee datos de una representación de cadena de texto, un archivo, una URL o un objeto de tipo
archivo JSON (JavaScript Object Notation: notación de objeto JavaScript).
read_pickle Lee un objeto almacenado por pandas empleando el formato pickle de Python.
read_sas Lee un conjunto de datos SAS almacenado en uno de los formatos de almacenamiento
personalizado del sistema SAS.
read_sql_table Lee una tabla SQL completa (utilizando SQLAlchemy); equivale a usar una consulta que lo
selecciona todo en la tabla mediante read_sql.
Debido a lo desordenados que pueden estar los datos en la realidad, parte de las
funciones de carga de datos (especialmente pandas.read_csv) han ido acumulando
con el tiempo una larga lista de argumentos opcionales. Es normal sentirse superado
por la cantidad de parámetros diferentes (pandas.read_csv tiene unos 50). La
documentación en línea de pandas ofrece muchos ejemplos sobre cómo funcionan
cada uno de ellos, de modo que si la lectura de algún archivo en particular da
problemas, quizá haya un ejemplo lo bastante similar que ayude a localizar los
parámetros adecuados.
Algunas de estas funciones realizan inferencia de tipos, porque los tipos de datos
de las columnas no son parte del formato de datos. Esto significa que no
necesariamente hay que especificar qué columnas son numéricas, enteras, booleanas o
de cadena de texto. Otros formatos de datos, como HDF5, ORC y Parquet, tienen la
información de los tipos de datos incrustada en el formato.
Manejar datos y otros tipos personalizados puede requerir un esfuerzo adicional.
Empecemos con un pequeño archivo de texto CSV (Comma-Separated Values), o
de valores separados por comas.
In [10]: !cat examples/ex1.csv
a,b,c,d,message
1,2,3,4,hello
5,6,7,8,world
9,10,11,12,foo
Aquí he utilizado el comando cat del shell de Unix para imprimir en pantalla el contenido del archivo sin
procesar. Trabajando en Windows se puede utilizar en su lugar type dentro de una línea de comandos de
Windows para lograr el mismo efecto.
Como está delimitado por comas, podemos usar entonces pandas.read_csv para
leerlo en un dataframe:
In [11]: df = pd.read_csv(“examples/ex1.csv”)
In [12]: df
Out[12]:
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo
Para leer este archivo tenemos un par de opciones. Podemos permitir que pandas
asigne nombres de columna por defecto, o bien podemos especificar nosotros los
nombres:
In [14]: pd.read_csv(“examples/ex2.csv”, header=None)
Out[14]:
0 1 2 3 4
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo
Supongamos que queremos que la columna message sea el índice del dataframe
devuelto. Podríamos o bien indicar que queremos la columna en el índice 4 o que se
llame “message” utilizando el argumento index_col:
In [16]: names = [“a”, “b”, “c”, “d”, “message”]
a b c d
message
hello 1 2 3 4
world 5 6 7 8
foo 9 10 11 12
In [20]: parsed
Out[20]:
value1 value2
key1 key2
one a 1 2
b 3 4
c 5 6
d 7 8
two a 9 10
b 11 12
c 13 14
d 15 16
En algunos casos, una tabla puede no tener un delimitador fijo y usar espacios en
blanco o algún otro sistema para separar campos. Veamos un archivo de texto
parecido a este:
In [21]: !cat examples/ex3.txt
A B C
aaa -0.264438 -1.026059 -0.619500
bbb 0.927272 0.302904 -0.032399
ccc -0.264273 -0.386314 -0.217601
ddd -0.871858 -0.348382 1.100491
Aunque se podría procesar manualmente, aquí los campos están separados por una
cantidad variable de espacios en blanco. En estos casos, se puede pasar una expresión
regular como delimitador para pandas.read_csv. Esto puede expresarse con la
expresión regular \s+, de modo que tenemos:
In [22]: result = pd.read_csv(“examples/ex3.txt”, sep=”\s+”)
In [23]: result
Out[23]:
A B C
aaa -0.264438 -1.026059 -0.619500
bbb 0.927272 0.302904 -0.032399
ccc -0.264273 -0.386314 -0.217601
ddd -0.871858 -0.348382 1.100491
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo
Manejar valores ausentes es una parte importante del proceso de lectura, a la que
se suele prestar poca atención. Puede ocurrir que los datos que faltan o bien no están
presentes (cadena de texto vacía) o están siendo marcados por algún valor centinela
(marcador). De forma predeterminada, pandas usa distintos centinelas, como NA y
NULL:
In [28]: result
Out[28]:
something a b c d message
0 one 1 2 3.0 4 NaN
1 two 5 6 NaN 8 world
2 three 9 10 11.0 12 foo
Conviene recordar que pandas muestra los valores ausentes como NaN, de modo
que tenemos dos valores nulos o faltantes en result:
In [29]: pd.isna(result)
Out[29]:
something a b c d message
0 False False False False False True
1 False False False True False False
2 False False False False False False
In [31]: result
Out[31]:
something a b c d message
0 one 1 2 3.0 4 NaN
1 two 5 6 NaN 8 world
2 three 9 10 11.0 12 foo
In [33]: result2
Out[33]:
something a b c d message
0 one 1 2 3 4 NA
1 two 5 6 8 world
2 three 9 10 11 12 foo
In [34]: result2.isna()
Out[34]:
something a b c d message
0 False False False False False False
1 False False False False False False
2 False False False False False False
....: na_values=[“NA”])
In [36]: result3
Out[36]:
something a b c d message
0 one 1 2 3 4 NaN
1 two 5 6 8 world
2 three 9 10 11 12 foo
In [37]: result3.isna()
Out[37]:
something a b c d message
0 False False False False False True
1 False False False False False False
2 False False False False False False
Argumento Descripción
path Cadena de texto que indica una ubicación en el sistema de archivos, una URL o un objeto
de tipo archivo.
sep o delimiter Secuencia de caracteres o expresión regular que se emplea para dividir campos en cada fila.
header Número de fila a utilizar como nombres de columna; por defecto es 0 (primera fila), pero
debería ser None si no hay fila de encabezado.
index_col Números o nombres de columna a utilizar como índice de fila en el resultado; puede ser un
solo nombre/número o una lista de ellos para un índice jerárquico.
skiprows Número de filas al comienzo del archivo que hay que ignorar o lista de números de fila
(empezando por 0) que hay que saltar.
na_values Secuencia de valores para reemplazar por NA. Se añaden a la lista predeterminada a menos
que se pase keep_default_na=False.
parse_dates Intenta analizar datos en datetime; es False por defecto. Si es True, intentará analizar
todas las columnas. En otro caso, puede especificar una lista de números o nombres de
columna para analizar. Si el elemento de la lista es una tupla u otra lista, combinará varias
columnas y analizará a la fecha (es decir, si la fecha u hora se divide entre dos columnas).
keep_date_col Si se unen columnas para analizar la fecha, mantiene las columnas unidas; es False por
defecto.
converters Diccionario que contiene un número o nombre de columna asignado a funciones (es decir,
{"foo": f} aplicaría la función f a todos los valores de la columna "foo").
dayfirst Cuando se analizan fechas potencialmente ambiguas, se trata como si tuvieran formato
internacional (por ejemplo, 7/6/2012 -> 7 de junio de 2012); es False por defecto.
nrows Número de filas que se leen desde el principio del archivo (sin contar el encabezado).
Argumento Descripción
iterator Devuelve un objeto TextFileReader para leer el archivo por partes. Este objeto se puede
utilizar también con la sentencia with.
verbose Imprime diversa información de análisis, como el tiempo empleado en cada etapa de la
conversión del archivo e información del uso de la memoria.
encoding Codificación de texto (por ejemplo, "utf-8" para texto codificado en UTF-8). Su valor
predeterminado es "utf-8" si es None.
squeeze Si los datos analizados contienen solo una columna, devuelve una serie.
thousands Separador de miles (es decir, "," o "."); por defecto es None.
decimal Separador decimal en números (es decir, "." o ","); por defecto es ".".
engine Motor de conversión y análisis CSV; puede ser "c", "python" o "pyarrow". El valor
predeterminado es "c", aunque el motor "pyarrow" más reciente puede analizar archivos
mucho más rápido. El motor "python" es más lento, pero soporta funciones que los otros
motores no admiten.
Ahora tenemos:
In [41]: result = pd.read_csv(“examples/ex6.csv”)
In [42]: result
Out[42]:
El signo de puntos suspensivos ... indica que las filas del centro del dataframe se
han omitido.
Si queremos leer solo una pequeña cantidad de filas (evitando leer el archivo
entero), lo especificamos con nrows:
In [43]: pd.read_csv(“examples/ex6.csv”, nrows=5)
Out[43]:
In [45]: type(chunker)
Out[45]: pandas.io.parsers.readers.TextFileReader
E 368.0
X 364.0
L 346.0
O 343.0
Q 340.0
M 338.0
J 337.0
F 335.0
K 334.0
H 330.0
dtype: float64
In [49]: data
Out[49]:
something a b c d message
0 one 1 2 3.0 4 NaN
1 two 5 6 NaN 8 world
2 three 9 10 11.0 12 foo
Utilizando el método to_csv del objeto DataFrame, podemos escribir los datos en
un archivo separado por comas:
In [50]: data.to_csv(“examples/out.csv”)
2|three|9|10|11.0|12|foo
Los valores que faltan se ven como cadenas de texto vacías en el resultado. Quizá
interese indicarlos con algún otro valor de centinela:
In [54]: data.to_csv(sys.stdout, na_rep=”NULL”)
,something,a,b,c,d,message
0,one,1,2,3.0,4,NULL
1,two,5,6,NULL,8,world
2,three,9,10,11.0,12,foo
three,9,10,11.0,12,foo
“1”,”2”,”3”
In [59]: f = open(“examples/ex7.csv”)
Iterar por el lector como un archivo produce listas de valores eliminando los que
van entre comillas:
A partir de ahí, es decisión propia realizar los procesos necesarios para dar a los
datos la forma que necesitamos. Hagamos esto paso a paso. Primero, leemos el
archivo en una lista de líneas:
In [63]: with open(“examples/ex7.csv”) as f:
In [66]: data_dict
Out[66]: {‘a’: (‘1’, ‘1’), ‘b’: (‘2’, ‘2’), ‘c’: (‘3’, ‘3’)}
Los archivos CSV existen en muchas clases distintas. Para definir un nuevo
formato con distinto delimitador, un convenio de entrecomillado de cadenas de texto
o un finalizador de línea, podríamos definir simplemente una subclase de
csv.Dialect:
class my_dialect(csv.Dialect):
lineterminator = “\n”
delimiter = “;”
quotechar = ‘”’
quoting = csv.QUOTE_MINIMAL
reader = csv.reader(f, dialect=my_dialect)
Argumento Descripción
delimiter Cadena de texto de un solo carácter para separar campos; su valor por defecto es ",".
lineterminator Terminador de línea para escritura; por defecto es "\r\n". El lector lo ignora y reconoce
los terminadores de línea de plataforma cruzada.
quotechar Carácter de comillas para campos con caracteres especiales (como un delimitador); el
valor predeterminado es '"'.
skipinitialspace Ignora los espacios en blanco tras cada delimitador; el valor predeterminado es False.
doublequote Cómo gestionar el carácter de comillas dentro de un campo; si es True, se duplica (en la
documentación en línea se puede consultar su comportamiento y resto de información).
escapechar Cadena de texto para quitar el delimitador si quoting está fijado en csv.QUOTE_NONE;
deshabilitado de forma predeterminada.
Para archivos con delimitadores de varios caracteres más complicados o fijos, no será posible usar el módulo
csv. En esos casos, habrá que dividir las líneas y realizar otros arreglos utilizando el método string de la
cadena de texto o el método de expresión regular re.split. Afortunadamente, pandas.read_csv es capaz de
hacer casi todo lo necesario si se pasan las opciones correspondientes, de modo que solamente en pocos
casos hará falta analizar archivos a mano.
Datos JSON
JSON, que significa JavaScript Object Notation (notación de objetos de
JavaScript), se ha convertido en uno de los formatos estándares para enviar datos
mediante petición HTTP entre navegadores web y otras aplicaciones. Es un formato
de datos mucho menos rígido que un formato de texto tabular como CSV. Aquí
tenemos un ejemplo:
obj = “””
{“name”: “Wes”,
“cities_lived”: [“Akron”, “Nashville”, “New York”, “San
Francisco”],
“pet”: null,
“siblings”: [{“name”: “Scott”, “age”: 34, “hobbies”: [“guitars”,
“soccer”]},
{“name”: “Katie”, “age”: 42, “hobbies”: [“diving”, “art”]}]
}
“””
JSON es código de Python casi perfectamente válido, con la excepción de su valor
nulo null y otras menudencias (como no permitir comas al final de las listas). Los
tipos básicos son objetos (dicccionarios), arrays (listas), cadenas de texto, números,
valores booleanos y nulos. Todas las claves de un objeto deben ser cadenas de texto.
Hay varias librerías de Python para leer y escribir datos JSON. Utilizaremos aquí
json, ya que está integrada en la librería estándar de Python. Para convertir una
cadena de texto JSON a su forma Python, empleamos json.loads:
In [68]: import json
In [70]: result
Out[70]:
{‘name’: ‘Wes’,
‘cities_lived’: [‘Akron’, ‘Nashville’, ‘New York’, ‘San
Francisco’],
‘pet’: None,
‘siblings’: [{‘name’: ‘Scott’,
‘age’: 34,
‘hobbies’: [‘guitars’, ‘soccer’]},
{‘name’: ‘Katie’, ‘age’: 42, ‘hobbies’: [‘diving’, ‘art’]}]}
In [72]: asjson
Out[72]: ‘{“name”: “Wes”, “cities_lived”: [“Akron”, “Nashville”,
“New York”, “San
In [74]: siblings
Out[74]:
name age
0 Scott 34
1 Katie 42
In [77]: data
Out[77]:
a b c
0 1 2 3
1 4 5 6
2 7 8 9
In [81]: len(tables)
Out[81]: 1
In [83]: failures.head()
Out[83]:
In [85]: close_timestamps.dt.year.value_counts()
Out[85]:
2010 157
2009 140
2011 92
2012 51
2008 25
...
2004 4
2001 4
2007 3
2003 3
2000 2
Name: Closing Date, Length: 15, dtype: int64
XML es otro formato habitual de datos estructurados que soporta datos jerárquicos
y anidados con metadatos. Este libro fue creado en realidad a partir de una serie de
documentos XML de gran tamaño.
Anteriormente he hablado de la función pandas.read_html, que utiliza lxml o
Beautiful Soup en segundo plano para analizar datos de HTML. XML y HTML son
estructuralmente similares, pero XML es más general. Aquí voy a mostrar un ejemplo
de cómo utilizar lxml para analizar datos en un formato XML más general.
Durante muchos años, la MTA de Nueva York, o Autoridad Metropolitana del
Transporte (Metropolitan Transportation Authority) estuvo publicando distintas series
de datos sobre sus servicios de autobús y tren en formato XML. Vamos a ver aquí los
datos de rendimiento, contenidos en varios archivos XML. Cada servicio de tren y
autobús tiene un archivo distinto (como por ejemplo Performance_MNR.xml para el
Metro-North Railroad), que incluye datos mensuales, como una serie de registros
XML, con este aspecto:
<INDICATOR>
<INDICATOR_SEQ>373889</INDICATOR_SEQ>
<PARENT_SEQ></PARENT_SEQ>
<AGENCY_NAME>Metro-North Railroad</AGENCY_NAME>
<INDICATOR_NAME>Escalator Availability</INDICATOR_NAME>
<DESCRIPTION>Percent of the time that escalators are operational
systemwide. The availability rate is based on physical observations
performed
the morning of regular business days only. This is a new indicator
the agency
began reporting in 2009.</DESCRIPTION>
<PERIOD_YEAR>2011</PERIOD_YEAR>
<PERIOD_MONTH>12</PERIOD_MONTH>
<CATEGORY>Service Indicators</CATEGORY>
<FREQUENCY>M</FREQUENCY>
<DESIRED_CHANGE>U</DESIRED_CHANGE>
<INDICATOR_UNIT>%</INDICATOR_UNIT>
<DECIMAL_PLACES>1</DECIMAL_PLACES>
<YTD_TARGET>97.00</YTD_TARGET>
<YTD_ACTUAL></YTD_ACTUAL>
<MONTHLY_TARGET>97.00</MONTHLY_TARGET>
<MONTHLY_ACTUAL></MONTHLY_ACTUAL>
</INDICATOR>
data = []
skip_fields = [“PARENT_SEQ”, “INDICATOR_SEQ”,
“DESIRED_CHANGE”, “DECIMAL_PLACES”]
for elt in root.INDICATOR:
el_data = {}
for child in elt.getchildren():
if child.tag in skip_fields:
continue
el_data[child.tag] = child.pyval
data.append(el_data)
In [92]: perf.head()
Out[92]:
AGENCY_NAME INDICATOR_NAME \
DESCRIPTION \
PERIOD_YEAR INDICATOR_UNIT
PERIOD_MONTH CATEGORY FREQUENCY
\
0 Service
2008 1 M %
Indicators
1 Service
2008 2 M %
Indicators
2 Service
2008 3 M %
Indicators
3 Service
2008 4 M %
Indicators
4 Service
2008 5 M %
Indicators
In [94]: perf2.head()
Out[94]:
INDICATOR_NAME \
DESCRIPTION \
CATEGORY DESIRED_CHANGE
PERIOD_YEAR PERIOD_MONTH
FREQUENCY \
0 Service U
2008 1 M
Indicators
1 Service U
2008 2 M
Indicators
2 Service U
2008 3 M
Indicators
3 Service U
2008 4 M
Indicators
4 Service U
2008 5 M
Indicators
MONTHLY_ACTUAL
0 96.90
1 95.00
2 96.90
3 98.30
4 95.80
In [96]: frame
Out[96]:
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo
In [97]: frame.to_pickle(“examples/frame_pickle”)
En general, los archivos pickle solo son legibles en Python. Se puede leer
cualquier objeto con este formato almacenado en un archivo empleando el método
pickle interno directamente, o incluso de un modo más conveniente con
pandas.read_pickle:
In [98]: pd.read_pickle(“examples/frame_pickle”)
Out[98]:
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo
pickle solo se recomienda como formato de almacenamiento a corto plazo. El problema es que es difícil
garantizar que el formato sea estable con el paso del tiempo; un objeto que está hoy en formato pickle puede
no estarlo con una versión posterior de una librería. pandas ha intentado mantener la compatibilidad con
versiones anteriores cuando ha sido posible, pero en algún momento del futuro puede resultar necesario
«romper» el formato pickle.
pandas tiene soporte integrado para otros formatos de datos binarios de código
abierto, como HDF5, ORC y Apache Parquet. Por ejemplo, si se instala el paquete
pyarrow (conda install pyarrow), entonces se pueden leer archivos Parquet con
pandas.read_parquet:
Los datos almacenados en una hoja se pueden leer entonces en un dataframe con
parse:
In [103]: xlsx.parse(sheet_name=”Sheet1”)
Out[103]:
0 a b c d message
Unnamed:
0 0 1 2 3 4 hello
1 1 5 6 7 8 world
2 2 9 10 11 12 foo
Esta tabla de Excel tiene una columna índice, de modo que podemos indicarlo con
el argumento index_col:
In [104]: xlsx.parse(sheet_name=”Sheet1”, index_col=0)
Out[104]:
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo
In [106]: frame
Out[106]:
Unnamed: 0 a b c d message
0 0 1 2 3 4 hello
1 1 5 6 7 8 world
2 2 9 10 11 12 foo
In [109]: writer.save()
Hay que tener en cuenta que el paquete PyTables se llama “tables” en PyPI, de modo que si se instala
con pip, será necesario ejecutar pip install tables.
In [117]: store
Out[117]:
<class ‘pandas.io.pytables.HDFStore’>
a
10 1.007189
11 -1.296221
12 0.274992
13 0.228913
14 1.352917
15 0.886429
In [121]: store.close()
La función put es una versión explícita del método store[“obj2”] = frame, pero
nos permite establecer otras opciones, como el formato de almacenamiento.
La función pandas.read_hdf ofrece una vía rápida para utilizar estas
herramientas:
In [122]: frame.to_hdf(“examples/mydata.h5”, “obj3”, format=”table”)
In [125]: os.remove(“examples/mydata.h5”)
Si estamos procesando datos que se almacenan en servidores remotos, como Amazon S3 o HDFS, en este
caso quizá sería más adecuado emplear otro formato binario diseñado para almacenamiento distribuido,
como Apache Parquet (http://parquet.apache.org).
HDF5 no es una base de datos, sino que es más adecuada para conjuntos de datos de una sola escritura y
varias lecturas. Aunque se pueden añadir datos a un archivo en cualquier momento, si se producen varias
escrituras al mismo tiempo, el archivo puede resultar dañado.
Para encontrar los últimos 30 temas de pandas en GitHub, podemos hacer una
petición GET de HTTP mediante la librería requests adicional:
In [126]: import requests
In [127]: url = “https://api.github.com/repos/pandas-
dev/pandas/issues”
In [129]: resp.raise_for_status()
In [130]: resp
Out[130]: <Response [200]>
In [132]: data[0][“title”]
Out[132]: ‘REF: make copy keyword non-stateful’
Como los resultados recuperados se basan en datos en tiempo real, lo que vea cada
usuario al ejecutar este código será casi seguro distinto.
Cada elemento de data es un diccionario que contiene todos los datos hallados en
una página de temas GitHub (salvo los comentarios). Podemos pasar los datos
directamente a pandas.DataFrame y extraer los campos que nos interesen:
In [133]: issues = pd.DataFrame(data, columns=[“number”, “title”,
.....: “labels”, “state”])
In [134]: issues
Out[134]:
number \
0 48062
1 48061
2 48060
3 48059
4 48058
.. ...
25 48032
26 48030
27 48028
28 48027
29 48026
title \
0 REF: make copy keyword non-stateful
1 STYLE: upgrade flake8
2 DOC: “Creating a Python environment” in “Creating a
development environ...
3 REGR: Avoid overflow with groupby sum
4 REGR: fix reset_index (Index.insert) regression with custom
Index subcl...
.. ...
25 BUG: Union of multi index with EA types can lose EA dtype
26 ENH: Add rolling.prod()
27 CLN: Refactor groupby’s _make_wrapper
28 ENH: Support masks in groupby prod
29 DEP: Add pip to environment.yml
labels \
0 []
1 [{‘id’: 106935113, ‘node_id’: ‘MDU6TGFiZ
WwxMDY5MzUxMTM=’, ‘url’: ‘https...
2 [{‘id’: 134699, ‘node_id’: ‘MDU6TGFiZWwxMzQ2OTk=’, ‘url’:
‘https://api....
3 [{‘id’: 233160, ‘node_id’: ‘MDU6TGFiZWwyMzMxNjA=’, ‘url’:
‘https://api....
4 [{‘id’: 32815646, ‘node_id’: ‘MDU6TGFiZWwzMjgxNTY0Ng==’,
‘url’: ‘https:...
.. ...
25 [{‘id’: 76811, ‘node_id’: ‘MDU6TGFiZWw3NjgxMQ==’, ‘url’:
‘https://api.g...
26 [{‘id’: 76812, ‘node_id’: ‘MDU6TGFiZWw3NjgxMg==’, ‘url’:
‘https://api.g...
27 [{‘id’: 233160, ‘node_id’: ‘MDU6TGFiZWwyMzMxNjA=’, ‘url’:
‘https://api....
28 [{‘id’: 233160, ‘node_id’: ‘MDU6TGFiZWwyMzMxNjA=’, ‘url’:
‘https://api....
29 [{‘id’: 76811, ‘node_id’: ‘MDU6TGFiZWw3NjgxMQ==’, ‘url’:
‘https://api.g...
state
0 open
1 open
2 open
3 open
4 open
.. ...
25 open
26 open
27 open
28 open
29 open
[30 rows x 4 columns]
Con un poco de esfuerzo se pueden crear ciertas interfaces de alto nivel para API
web habituales, que devuelven objetos DataFrame para un análisis más conveniente.
In [136]:
query = “””
.....: CREATE TABLE test
.....: (a VARCHAR(20), b VARCHAR(20),
.....: c REAL, d INTEGER
.....: );”””
In [138]: con.execute(query)
Out[138]: <sqlite3.Cursor at 0x7fdfd73b69c0>
In [139]: con.commit()
In [143]: con.commit()
In [146]: rows
Out[146]:
a b c d
0 Atlanta Georgia 1.25 6
1 Tallahassee Florida 2.60 3
2 Sacramento California 1.70 5
Quizá sea preferible no repetir este procesado cada vez que se consulta la base de
datos. El proyecto SQLAlchemy (http://www.sqlalchemy.org/) es un conocido kit
de herramientas SQL de Python que resume muchas de las diferencias normales entre
bases de datos SQL. pandas tiene una función read_sql que permite leer datos
fácilmente desde una conexión SQLAlchemy general. Se puede instalar
SQLAlchemy con conda del siguiente modo:
conda install sqlalchemy
In [150]: db = sqla.create_engine(“sqlite:///mydata.sqlite”)
a b c d
0 Atlanta Georgia 1.25 6
1 Tallahassee Florida 2.60 3
2 Sacramento California 1.70 5
6.5 Conclusión
Obtener acceso a los datos suele ser el primer paso en el proceso de análisis de
datos. Hemos visto en este capítulo distintas herramientas útiles para tal objetivo. En
los siguientes capítulos profundizaremos en la disputa y visualización de datos, en el
análisis de series temporales y en otros temas.
1
Para consultar la lista completa véase https://www.fdic.gov/bank/individual/failed/banklist.html.
Capítulo 7
Limpieza y preparación de los datos
In [15]: float_data
Out[15]:
0 1.2
1 -3.5
2 NaN
3 0.0
dtype: float64
El método isna nos da una serie booleana con True, en la que los
valores son nulos:
In [16]: float_data.isna()
Out[16]:
0 False
1 False
2 True
3 False
dtype: bool
In [18]: string_data
Out[18]:
0 aardvark
1 NaN
2 None
3 avocado
dtype: object
In [19]: string_data.isna()
Out[19]:
0 False
1 True
2 True
3 False
dtype: bool
In [21]: float_data
Out[21]:
0 1.0
1 2.0
2 NaN
dtype: float64
In [22]: float_data.isna()
Out[22]:
0 False
1 False
2 True
dtype: bool
Método Descripción
dropna Filtra etiquetas de eje basándose en si los valores de cada etiqueta tienen datos
ausentes, con diversos umbrales para la cantidad de datos nulos que soportar.
fillna Rellena datos faltantes con algún valor o utilizando un método de interpolación como
"ffill" o "bfill".
isna Devuelve valores booleanos indicando qué valores están ausentes o son nulos.
notna Negación de isna, devuelve True para valores que no son nulos y False para los que lo
son.
0 1.0
2 3.5
4 7.0
dtype: float64
0 1.0
2 3.5
4 7.0
dtype: float64
Con objetos DataFrame, hay distintas maneras de eliminar los datos que
faltan. Quizá interese eliminar las filas o columnas que son todas nulas, o
solamente las filas o columnas que contengan algún valor nulo. dropna
elimina por defecto cualquier fila que contiene un valor faltante:
In [26]: data = pd.DataFrame([[1., 6.5, 3.], [1., np.nan,
np.nan],
....: [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
In [27]: data
Out[27]:
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
2 NaN NaN NaN
3 NaN 6.5 3.0
In [28]: data.dropna()
Out[28]:
0 1 2
0 1.0 6.5 3.0
Pasar how=”all” quitará solamente las filas que sean todas nulas:
In [29]: data.dropna(how=”all”)
Out[29]:
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
3 NaN 6.5 3.0
In [31]: data
Out[31]:
0 1 2 4
0 1.0 6.5 3.0 NaN
1 1.0 NaN NaN NaN
2 NaN NaN NaN NaN
3 NaN 6.5 3.0 NaN
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
2 NaN NaN NaN
3 NaN 6.5 3.0
Supongamos que queremos mantener solo filas que contengan como
máximo un cierto número de observaciones faltantes. Se puede indicar esto
con el argumento thresh:
In [33]: df = pd.DataFrame(np.random.standard_normal((7,
3)))
In [36]: df
Out[36]:
0 1 2
0 -0.204708 NaN NaN
1 -0.555730 NaN NaN
2 0.092908 NaN 0.769023
3 1.246435 NaN -1.296221
4 0.274992 0.228913 1.352917
5 0.886429 -2.001637 -0.371843
6 1.669025 -0.438570 -0.539741
In [37]: df.dropna()
Out[37]:
0 1 2
4 0.274992 0.228913 1.352917
5 0.886429 -2.001637 -0.371843
6 1.669025 -0.438570 -0.539741
In [38]: df.dropna(thresh=2)
Out[38]:
0 1 2
2 0.092908 NaN 0.769023
3 1.246435 NaN -1.296221
4 0.274992 0.228913 1.352917
5 0.886429 -2.001637 -0.371843
6 1.669025 -0.438570 -0.539741
Rellenado de datos ausentes
En lugar de filtrar datos ausentes (y posiblemente arrastrar con ellos
otros datos), quizá sea más conveniente rellenar los “huecos” de distintas
maneras. Para la mayoría de los casos se debe emplear el método fillna.
Llamar a fillna con una constante reemplaza los valores ausentes por
dicho valor:
In [39]: df.fillna(0)
Out[39]:
0 1 2
0 -0.204708 0.000000 0.000000
1 -0.555730 0.000000 0.000000
2 0.092908 0.000000 0.769023
3 1.246435 0.000000 -1.296221
4 0.274992 0.228913 1.352917
5 0.886429 -2.001637 -0.371843
6 1.669025 -0.438570 -0.539741
0 1 2
0 -0.204708 0.500000 0.000000
1 -0.555730 0.500000 0.000000
2 0.092908 0.500000 0.769023
3 1.246435 0.500000 -1.296221
4 0.274992 0.228913 1.352917
5 0.886429 -2.001637 -0.371843
6 1.669025 -0.438570 -0.539741
In [44]: df
Out[44]:
0 1 2
0 0.476985 3.248944 -1.021228
1 -0.577087 0.124121 0.302614
2 0.523772 NaN 1.343810
3 -0.713544 NaN -2.370232
4 -1.860761 NaN NaN
5 -1.265934 NaN NaN
In [45]: df.fillna(method=”ffill”)
Out[45]:
0 1 2
0 0.476985 3.248944 -1.021228
1 -0.577087 0.124121 0.302614
2 0.523772 0.124121 1.343810
3 -0.713544 0.124121 -2.370232
4 -1.860761 0.124121 -2.370232
5 -1.265934 0.124121 -2.370232
0 1 2
0 0.476985 3.248944 -1.021228
1 -0.577087 0.124121 0.302614
2 0.523772 0.124121 1.343810
3 -0.713544 0.124121 -2.370232
4 -1.860761 NaN -2.370232
5 -1.265934 NaN -2.370232
Con fillna se pueden hacer muchas otras cosas, como la imputación de
datos sencilla utilizando las estadísticas de mediana o media:
In [47]: data = pd.Series([1., np.nan, 3.5, np.nan, 7])
In [48]: data.fillna(data.mean())
Out[48]:
0 1.000000
1 3.833333
2 3.500000
3 3.833333
4 7.000000
dtype: float64
Argumento Descripción
value Valor escalar u objeto de tipo diccionario que se utiliza para rellenar valores
faltantes.
method Método de interpolación: puede ser "bfill" (relleno hacia atrás) o "ffill" (relleno
hacia delante); el valor predeterminado es None.
limit Para relleno hacia delante o hacia atrás, máximo número de períodos consecutivos
que rellenar.
In [50]: data
Out[50]:
k1 k2
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
5 two 4
6 two 4
False
0
1 False
2 False
3 False
4 False
5 False
6 True
dtype: bool
Algo parecido hace drop_duplicates, que devuelve un dataframe con
filas en las que el array duplicate es False al ser filtrado:
In [52]: data.drop_duplicates()
Out[52]:
k1 k2
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
5 two 4
In [54]: data
Out[54]:
k1 k2 v1
0 one 1 0
1 two 1 1
2 one 2 2
3 two 3 3
4 one 3 4
5 two 4 5
6 two 4 6
In [55]: data.drop_duplicates(subset=[“k1”])
Out[55]:
k1 k2 v1
0 one 1 0
1 two 1 1
k1 k2 v1
0 one 1 0
1 two 1 1
2 one 2 2
3 two 3 3
4 one 3 4
6 two 4 6
In [58]: data
Out[58]:
food ounces
0 bacon 4.0
1 pulled pork 3.0
2 bacon 12.0
3 pastrami 6.0
4 corned beef 7.5
5 bacon 8.0
6 pastrami 3.0
7 honey ham 5.0
8 nova 6.0
meat_to_animal = {
“bacon”: “pig”,
“pulled pork”: “pig”,
“pastrami”: “cow”,
“corned beef”: “cow”,
“honey ham”: “pig”,
“nova lox”: “salmon”
}
In [61]: data
Out[61]:
También podríamos haber pasado una función que haga todo el trabajo:
In [62]: def get_animal(x):
....: return meat_to_animal[x]
In [63]: data[“food”].map(get_animal)
Out[63]:
pig
0
1 pig
2 pig
3 cow
4 cow
5 pig
6 cow
7 pig
8 salmon
Name: food, dtype: object
Reemplazar valores
Rellenar datos ausentes con el método fillna es un caso especial del
reemplazo de valores más general. Como ya hemos visto, map se puede
utilizar para modificar un subconjunto de valores de un objeto, pero
replace ofrece una forma más sencilla y flexible de hacerlo. Veamos esta
serie:
In [64]: data = pd.Series([1., -999., 2., -999., -1000.,
3.])
In [65]: data
Out[65]:
0 1.0
1 -999.0
2 2.0
3 -999.0
4 -1000.0
5 3.0
dtype: float64
Los valores -999 podrían ser valores centinela para datos ausentes. Para
reemplazarlos por valores NA que pandas comprenda, podemos utilizar
replace, produciendo una nueva serie:
1.0
0
1 NaN
2 2.0
3 NaN
4 -1000.0
5 3.0
dtype: float64
1.0
0
1 NaN
2 2.0
3 NaN
4 NaN
5 3.0
dtype: float64
Para utilizar un sustituto distinto para cada valor, pasamos una lista de
sustitutos:
In [68]: data.replace([-999, -1000], [np.nan, 0])
Out[68]:
1.0
0
1 NaN
2 2.0
3 NaN
4 0.0
5 3.0
dtype: float64
1.0
0
1 NaN
2 2.0
3 NaN
4 0.0
5 3.0
dtype: float64
Igual que en una serie, los índices de eje tienen un método map:
In [71]: def transform(x):
....: return x[:4].upper()
In [72]: data.index.map(transform)
Out[72]: Index([‘OHIO’, ‘COLO’, ‘NEW ‘], dtype=’object’)
In [74]: data
Out[74]:
Discretización
Los datos continuos se suelen discretizar o, lo que es lo mismo, separar
en «contenedores» para su análisis. Imaginemos que tenemos datos sobre
un grupo de personas de un estudio, y queremos agruparlos en sendos
contenedores por edad:
In [77]: ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41,
32]
In [80]: age_categories