{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# ¿Qué es el Data Science?\n",
    "\n",
    "El Data Science es un campo interdisciplinario que involucra métodos científicos, procesos y sistemas para extraer conocimiento o un mejor entendimiento de datos en sus diferentes formas, ya sea estructurados o no estructurados, lo cual es una continuación de algunos campos de análisis de datos como la estadística, la minería de datos, el aprendizaje automático y la analítica predictiva.\n",
    "\n",
    "El Data Science combina software, estadística, matemática, programación y visualización. Y su objetivo es extraer datos factibles de interpretarse e incluso crear nueva información. Las conclusiones que se obtienen permiten desarrollar productos demandados en el mercado o generar oportunidades de negocio de una empresa.\n",
    "\n",
    "\n",
    "# ¿Qué es pandas?\n",
    "\n",
    "El paquete **pandas** es la herramienta más importante a disposición de los científicos y analistas de datos que trabajan en Python en la actualidad. Las poderosas herramientas de aprendizaje automático y visualización glamorosa pueden llamar toda la atención, pero pandas es la columna vertebral de la mayoría de los proyectos de datos.\n",
    "\n",
    "En Computación y Ciencia de datos, pandas es una biblioteca de software escrita como extensión de Numpy para manipulación y análisis de datos para el lenguaje de programación Python. Esta librería ofrece dos de las estructuras más usadas en Data Science: la estructura Series y el DataFrame. En este curso veremos cómo crearlas, las herramientas básicas de uso y algunas de las funciones y métodos que nos permitirán extraer todo el potencial de ellas.\n",
    "\n",
    "# Las características de la biblioteca son:\n",
    "\n",
    "- El tipo de datos DataFrame para manipulación de datos con indexación integrada. Tiene herramientas para leer y escribir datos entre estructuras de datos en memoria y formatos de archivos variados\n",
    "- Permite la alineación de datos y manejo integrado de datos faltantes, la reestructuración y segmentación de conjuntos de datos, la segmentación vertical basada en etiquetas, indexación elegante, y segmentación horizontal de grandes conjuntos de datos, la inserción y eliminación de columnas en estructuras de datos.\n",
    "- Puedes realizar cadenas de operaciones, dividir, aplicar y combinar sobre conjuntos de datos, la mezcla y unión de datos.\n",
    "- Permite realizar indexación jerárquica de ejes para trabajar con datos de altas dimensiones en estructuras de datos de menor dimensión, la funcionalidad de series de tiempo: generación de rangos de fechas y conversión de frecuencias, desplazamiento de ventanas estadísticas y de regresiones lineales, desplazamiento de fechas y retrasos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Primeros pasos de Pandas\n",
    "\n",
    "## Instalar e importar\n",
    "\n",
    "Pandas es un paquete fácil de instalar. Abra su programa de terminal (para usuarios de Mac) o línea de comandos (para usuarios de PC) e instálelo usando cualquiera de los siguientes comandos:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "conda install pandas **o** pip install pandas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Dado que estamos trabajando sobre un cuaderno de Jupyter, puede ejecutar esta celda:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "! pip install pandas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El ! al principio ejecuta las celdas como si estuvieran en una terminal.\n",
    "\n",
    "Para importar pandas, generalmente lo importamos con un nombre más corto ya que se usa mucho:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Componentes principales de pandas: Series y DataFrames\n",
    "\n",
    "Los dos componentes principales de los pandas son Series y DataFrame.\n",
    "\n",
    "Una Series es esencialmente una columna y un DataFrame es una tabla multidimensional formada por una colección de Series.\n",
    "\n",
    "<img src=\"https://storage.googleapis.com/lds-media/images/series-and-dataframe.width-1200.png\">\n",
    "\n",
    "\n",
    "# Series\n",
    "\n",
    "Las series son estructuras unidimensionales conteniendo un array de datos (de cualquier tipo soportado por NumPy) y un array de etiquetas que van asociadas a los datos, llamado índice (index en la literatura en inglés):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas = pd.Series([15,12,21], index = [\"Ene\", \"Feb\", \"Mar\"])\n",
    "ventas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los elementos de la serie pueden extraerse con el nombre de la serie y, entre corchetes, el índice (posición) del elemento:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "o con su etiqueta, si la tiene: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas[\"Ene\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las etiquetas que forman el índice no necesitan ser diferentes. Pueden ser de cualquier tipo (numérico, textos, tuplas...) siempre que sea posible aplicar la función hash sobre ellas.\n",
    "\n",
    "hash : https://www.interactivechaos.com/python/function/hash\n",
    "\n",
    "Es de destacar que el lazo entre una etiqueta y un valor se mantendrá salvo que lo modifiquemos explícitamente. Esto quiere decir que filtrar una serie o eliminar un elemento de la serie, por ejemplo, no va a modificar las etiquetas asignadas a cada valor.\n",
    "\n",
    "Otro comentario importante es al respecto de la inmutabilidad del índice de etiquetas: aun cuando es posible asignar a una serie un nuevo conjunto de etiquetas a través del atributo index, intentar modificar un único valor del index va a devolver un error.\n",
    "\n",
    "Al igual que ocurre con el array NumPy, una serie pandas solo puede contener datos de un mismo tipo. En la imagen anterior puede apreciarse el índice a la izquierda (\"Ene\", \"Feb\" y \"Mar\") y los datos a la derecha (15, 12 y 21). El tipo de la serie, accesible a través del atributo dtype (Se muestra en la parte inferior: int64), coincide con el tipo de los datos que contiene:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas.dtype"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podemos acceder a los objetos que contienen los índices y los valores a través de los atributos **index** y **values** de la serie, respectivamente:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas.index"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas.values"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Puede apreciarse en el ejemplo que el índice es de tipo \"objeto\"."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La serie tiene, además, un atributo name, atributo que también encontramos en el índice. Una vez los hemos fijado, se muestran junto con la estructura al imprimir la serie:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas.name = \"Ventas 2020\"\n",
    "ventas.name"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas.index.name = \"Meses\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Obsérvese cómo, en este ultimo ejemplo, en la salida, tanto la serie como el índice se muestran con su nombre (\"Ventas 2020\" y \"Meses\", respectivamente)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El atributo **axes** nos da acceso a una lista con los ejes de la serie (solo contiene un elemento al tratarse de una estructura unidimensional):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas.axes"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El atributo **shape** nos devuelve el tamaño de la serie:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para ver mas atributos de las Series se puede consultar la documentación de pandas: https://pandas.pydata.org/pandas-docs/stable/reference/series.html"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# DataFrames \n",
    "\n",
    "Crear DataFrames directamente en Python y es bastante útil cuando se prueban nuevos métodos y funciones que se encuentran en los documentos de pandas.\n",
    "\n",
    "Hay muchas formas de crear un DataFrame desde cero, pero una gran opción es usar un simple **dict**.\n",
    "\n",
    "Digamos que tenemos un puesto de frutas que vende manzanas y naranjas. Queremos tener una columna para cada fruta y una fila para cada compra del cliente. Para organizar esto como un diccionario para pandas, podríamos hacer algo como:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "datos = { 'manzanas' : [ 3 , 2 , 0 , 1 ], 'naranjas' : [ 0 , 3 , 7 , 2 ] }"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y luego páselo al constructor de Pandas DataFrame:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "compras = pd.DataFrame( datos )\n",
    "compras"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### ¿Cómo funcionó eso?\n",
    "\n",
    "Cada elemento (clave, valor) en \"datos\" corresponde a una columna en el DataFrame resultante.\n",
    "\n",
    "El índice de este DataFrame se nos dio en la creación como los números 0-3, pero también podríamos crear el nuestro cuando inicializamos el DataFrame.\n",
    "\n",
    "Tengamos nombres de clientes como nuestro índice:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "compras = pd.DataFrame( datos , index = [ 'Juno' , 'Robert' , 'Lily' , 'David' ])   \n",
    "compras"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las etiquetas de filas y de columnas -los índices- son accesibles a través de los atributos index y columns, respectivamente:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "compras.index"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "compras.columns"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La nomenclatura usada por pandas puede resultar un tanto confusa en lo que se refiere a los índices: tanto la estructura que contiene las etiquetas de filas como la que contiene las etiquetas de columnas son objetos de tipo Index (\"índice\", en español), pero, como se ha comentado, el índice de filas se denomina también index (aunque en minúsculas), y el de columna, columns."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Además, el nombre de \"indice\" se aplica normalmente a la referencia de un dato en una estructura según su posición. Por ejemplo, en la lista m = [\"a\", \"b\"], el índice del primer elemento es el número o valor que, añadido entre corchetes tras el nombre de la lista, nos permite acceder al elemento. Así, el índice del elemento \"a\" en la lista mencionada es 0, y el índice del elemento \"b\" es 1, lo que no es del todo coherente con el concepto de \"índice\" de una estructura pandas cuando lo especificamos explícitamente."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para evitar esta confusión, hablaremos normalmente de \"índices\" (en plural) para referirnos a estas dos estructuras (de filas y columnas), de \"índice\" (en singular) para referirnos al índice de etiquetas del eje vertical, y de \"índice de columnas\" y de \"índice de filas\" siempre que sea necesario remarcar a cuál estamos refiriéndonos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "compras.index"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "compras.columns"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El eje 0 es el correspondiente al índice de filas (eje vertical) y el eje 1 al índice de columnas (eje horizontal). Como puede verse en el ejemplo anterior ejemplo, ambos índices son de tipo \"objeto\" (ya se ha comentado que, concretamente, son objetos de tipo Index).\n",
    "\n",
    "El atributo axes devuelve una lista con los ejes de la estructura (dos, al tratarse de una estructura bidimensional):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "compras.axes"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Al igual que ocurría con las series, los índices de filas y columnas son inmutables. Esto significa que, aunque podemos asignar un nuevo conjunto de datos (etiquetas) a ambas estructuras (index o columns), intentar modificar un único valor devolverá un error.\n",
    "\n",
    "Tanto el índice de filas como el de columnas poseen el atributo name. Una vez fijado, se muestra al imprimir la estructura:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "compras.index.name = \"Clientes\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "compras.columns.name = \"Frutas\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "compras"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De forma semejante a como ocurría con las series, el atributo values de un dataframe nos permite acceder a los valores del dataframe, con formato array NumPy 2d:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "compras.values"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este array tendrá un tipo u otro en función de los tipos de las columnas del dataframe, acomodándose de forma que englobe a todos ellos.\n",
    "\n",
    "Y un dataframe también tiene un atributo shape que nos informa de su dimensionalidad y del número de elementos en cada dimensión. En el siguiente ejemplo Podemos ver que el dataframe compras tiene 4 filas y 2 columnas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "compras.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para ver más información sobre DataFrame se puede consultar la documentación de pandas: https://pandas.pydata.org/pandas-docs/stable/reference/frame.html"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Creación de series\n",
    "\n",
    "El constructor para la creación de una serie pandas es pandas.Series. Este constructor acepta tres parámetros principales:\n",
    "\n",
    "- data: estructura de datos tipo array, iterable, diccionario o valor escalar que contendrá los valores a introducir en la serie.\n",
    "- index: estructura tipo array con la misma longitud que los datos. Si este argumento no se añade al crear la serie, se agregará un índice por defecto formado por números enteros desde 0 hasta n-1, siendo n el número de datos.\n",
    "- dtype: tipo de datos para la serie. Si no se especifica, se inferirá a partir de los datos.\n",
    "\n",
    "Los valores del índice, como ya se ha comentado anteriormente, no tienen que ser necesariamente distintos aunque ciertas operaciones pueden generar un error si no soportan la posibilidad de tener índices duplicados.\n",
    "\n",
    "\n",
    "### Utilizando una lista "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([7,5,3])\n",
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Al no haberse especificado un índice, se asigna uno automáticamente con los valores 0, 1 y 2."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si repetimos esta instrucción especificando un índice:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([7,5,3], index = [\"Ene\", \"Feb\", \"Mar\"])\n",
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí vemos cómo el índice por defecto ha sido sustituido por el indicado. En este caso, la longitud del índice deberá coindicir con el número de elementos de la lista.\n",
    "\n",
    "Los mismos comentarios podrían hacerse si, en lugar de una lista, hubiésemos partido de un array NumPy para crear la serie."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Utilizando un diccionario"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "d = {\"Ene\":7, \"Feb\":5, \"Mar\":3 }\n",
    "s = pd.Series(d)\n",
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí vemos cómo el constructor utiliza las claves como etiquetas del índice, y los valores del diccionario como valores de la serie.\n",
    "\n",
    "Si incluimos el índice explícitamente en el constructor, los valores en la serie se tomarán en el orden en el que estén en el índice explícito. Además, si en éste hay valores que no pertenecen al conjunto de claves del diccionario, se añaden a la serie con un valor NaN:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "d = {\"Ene\":7, \"Feb\":5, \"Mar\":3 }\n",
    "s = pd.Series(d, index = [\"Abr\", \"Mar\", \"Feb\", \"Ene\"],dtype=int)\n",
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este ejemplo, hemos creado la serie especificando el índice que hemos formado dando la vuelta a las claves del diccionario (\"Mar\", \"Feb\" y \"Ene\") y hemos añadido a la lista de etiquetas el valor \"Abr\", que no pertenece al conjunto de claves del diccionario. Se ha añadido a la serie, pero se le ha asignado el valor NaN. Es precisamente la presencia de este valor lo que modifica el tipo de la serie a float."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Utilizando un escalar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si los datos se reducen a un escalar (no a una lista con un único elemento, sino a un sencillo escalar como 7 o 15.4) será necesario añadir el índice explícitamente. El número de elementos de la serie coincidirá con el número de elementos del índice, y el escalar será asignado como valor a todos ellos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series(7, index = [\"Ene\", \"Feb\", \"Mar\"])\n",
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Creación de dataframes\n",
    "\n",
    "El constructor de dataframes es \"pandas.DataFrame\". Acepta cuatro parámetros principales:\n",
    "\n",
    "- data: estructura de datos ndarray (array NumPy), diccionario u otro dataframe\n",
    "- index: índice a aplicar a las filas. Si no se especifica, se asignará uno por defecto formado por números enteros entre 0 y n-1, siendo n el número de filas del dataframe.\n",
    "- columns: etiquetas a aplicar a las columnas. Al igual que ocurre con el índice de filas, si no se añade se asignará uno automático formado por números enteros entre 0 y n-1, siendo n el número de columnas.\n",
    "- dtype: tipo a aplicar a los datos. Solo se permite uno. Si no se especifica, se infiere el tipo de cada columna a partir de los datos que contengan.\n",
    "\n",
    "Los valores de los índices de filas y columnas no tienen por qué ser necesariamente distintos.\n",
    "\n",
    "Veamos algunas de las estructuras a partir de las que es posible construir un dataframe:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Utilizando un diccionario"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "elementos = { \n",
    "    \"Numero atómico\":[1, 6, 47, 88],\n",
    "    \"Masa atómica\":[1.008, 12.011, 107.87, 226],\n",
    "    \"Familia\":[\"No metal\", \"No metal\", \"Metal\", \"Metal\"]\n",
    "}\n",
    "elementos"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y creamos el dataframe con él como primer argumento:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tabla_periodica = pd.DataFrame(elementos)\n",
    "tabla_periodica"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El dataframe se ha creado situando las claves del diccionario como etiquetas de columnas y las listas asociadas a cada clave como columnas del dataframe. Al no haber especificado un índice de filas, éste ha tomado valores por defecto (0, 1, 2 y 3).\n",
    "\n",
    "A continuación repetimos la misma operación especificando las etiquetas tanto para filas como para columnas, utilizando los parámetros index y columns, respectivamente:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tabla_periodica = pd.DataFrame(elementos,\n",
    "                               index = [\"H\", \"C\", \"Ag\", \"Ra\"],\n",
    "                               columns = [\"Familia\", \"Numero atómico\", \"Masa atómica\"]\n",
    ")\n",
    "tabla_periodica"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Recordemos que con el parámetro columns podemos especificar el orden en el que se mostrarán las columnas o incluso filtrar éstas (no incluyendo todas las etiquetas presentes en el diccionario como claves), pero no cambiar sus nombres. De hecho, ya se ha comentado que si alguna de las etiquetas incluidas en dicho argumento no apareciese en el conjunto de claves del diccionario, se crearía una columna con dicho nombre pero con todos sus valores fijados a NaN.\n",
    "\n",
    "Si, en lugar de listas de datos como valores del diccionario, hubiesen sido arrays NumPy o series, el procedimiento habría sido exactamente el mismo.\n",
    "\n",
    "### Utilizando un array Numpy"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el caso de partir de un array NumPy, si no se especifican las etiquetas de filas y columnas, se asignan las etiquetas por defecto:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "unidades_Datos = np.array([[2, 5, 3, 2],\n",
    "                         [4, 6, 7, 2], \n",
    "                          [3, 2, 4, 1]])\n",
    "unidades_Datos"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "unidades = pd.DataFrame(unidades_Datos)\n",
    "unidades"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las filas del array NumPy siguen siendo interpretadas como filas del dataframe.\n",
    "\n",
    "Si especificamos las etiquetas de filas y columnas, el resultado es diferente:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "unidades = pd.DataFrame(unidades_Datos, index = [2015, 2016, 2017], columns = [\"Ag\", \"Au\", \"Cu\", \"Pt\"])\n",
    "unidades"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Utilizando diferentes diccionarios\n",
    "\n",
    "También podemos partir de un conjunto de diccionarios, cada uno definiendo el contenido de lo que será una fila del dataframe:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "unidades_2015 = {\"Ag\":2, \"Au\":5, \"Cu\":3, \"Pt\":2}\n",
    "unidades_2016 = {\"Ag\":4, \"Au\":6, \"Cu\":7, \"Pt\":2}\n",
    "unidades_2017 = {\"Ag\":3, \"Au\":2, \"Cu\":4, \"Pt\":1}\n",
    "\n",
    "unidades = pd.DataFrame([unidades_2015, unidades_2016, unidades_2017],\n",
    "                       index = [2015, 2016, 2017])\n",
    "unidades"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los diccionarios deberán compartir el mismo conjunto de claves que se interpretarán como etiquetas de columnas. Si las etiquetas no coinciden, se crearán todas las columnas pero se asignarán NaN a los valores desconocidos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "unidades_2015 = {\"Ag\":2, \"Au\":5, \"Cu\":3, \"Pt\":2}\n",
    "unidades_2016 = {\"Ag\":4, \"Au\":6, \"Cu\":7, \"Pt\":2}\n",
    "unidades_2017 = {\"Ag\":3, \"Pb\":2, \"Cu\":4, \"Pt\":1}\n",
    "\n",
    "unidades = pd.DataFrame([unidades_2015, unidades_2016, unidades_2017],\n",
    "                       index = [2015, 2016, 2017])\n",
    "unidades"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este ejemplo, el año 2017 tiene una clave, Pb, que no existe en los otros dos diccionarios. Y este mismo año carece de la clave Au que sí se encuentra en los otros dos. Vemos cómo los datos no coincidentes se han rellenado con NaN.\n",
    "\n",
    "# Otro metodos de contrucción\n",
    "\n",
    "- pandas.DataFrame.from_dict, crea un dataframe a partir de un diccionario de diccionarios o de secuencias tipo array:\n",
    "https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.from_dict.html\n",
    "\n",
    "- pandas.DataFrame.from_records, que parte de una lista de tuplas o de arrays NumPy con un tipo estructurado:\n",
    "https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.from_records.html"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Inspección de la información \n",
    "\n",
    "Normalmente, una vez hemos cargado un bloque de datos en una serie o un dataframe, lo primero que haremos será inspeccionarlo para confirmar que los datos cargados son los esperados y que la lectura se ha realizado correctamente. Para esto tenemos los métodos **head**, **tail** y **sample**, con un comportamiento semejante en series y dataframes, que nos muestran un subconjunto de los datos cargados. Además, los métodos **describe** e **info** nos proporcionan información adicional sobre los datos. Veamos estos métodos por separado."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El método head\n",
    "\n",
    "Este método, pandas.Series.head para series y pandas.DataFrame.head para dataframes, devuelve los primeros elementos de la estructura (los primeros valores en el caso de una serie y las primeras filas en el caso de un dataframe). Por defecto, se trata de los 5 primeros elementos, pero podemos especificar el número que deseamos como argumento de la función. Por ejemplo, partamos de las siguientes estructuras:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "entradas = pd.Series([11, 18, 12, 16, 9, 16, 22, 28, 31, 29, 30, 12],\n",
    "            index = [\"ene\", \"feb\", \"mar\", \"abr\", \"may\", \"jun\", \"jul\", \"ago\",\n",
    "             \"sep\", \"oct\", \"nov\", \"dic\"])\n",
    "entradas"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "salidas = pd.Series([9, 26, 18, 15, 6, 22, 19, 25, 34, 22, 21, 14],\n",
    "            index = [\"ene\", \"feb\", \"mar\", \"abr\", \"may\", \"jun\", \"jul\", \"ago\",\n",
    "             \"sep\", \"oct\", \"nov\", \"dic\"])\n",
    "salidas"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "almacén = pd.DataFrame({\"entradas\": entradas, \"salidas\": salidas})\n",
    "almacén[\"neto\"] = almacén.entradas - almacén.salidas\n",
    "almacén"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este ejemplo estamos mostrando todos los elementos de la estructura pues son apenas 12. En un caso real podemos estar hablando de miles o de millones.\n",
    "\n",
    "Ahora, para mostrar apenas los primeros elementos de la estructura, ejecutamos el método head:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "entradas.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "almacén.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El método tail \n",
    "\n",
    "Los métodos pandas.Series.tail (para series) y pandas.DataFrame.tail (para dataframes) son semejantes a los anteriores, pero muestran los últimos elementos de la estructura. Si no indicamos otra cosa como argumento, serán los 5 últimos elementos los que se muestren:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "entradas.tail()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "almacén.tail()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El método sample\n",
    "\n",
    "Sin embargo, es frecuente que los datos que hayamos leído estén ordenados según algún criterio, y que el bloque de datos mostrado por los métodos head o tail estén formados por datos muy parecidos. Y en ocasiones nos puede convenir ver datos aleatorios de nuestra estructura. Para esto podemos utilizar los métodos \"pandas.Series.sample\" para series y \"pandas.DataFrame.sample\" para dataframes. Al contrario que head o tail, el número de elementos devueltos por defecto es uno, por lo que, si deseamos extraer una muestra mayor, tendremos que indicarlo como primer argumento:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "entradas.sample()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "almacén.sample(5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El método describe\n",
    "\n",
    "El método describe devuelve información estadística de los datos del dataframe o de la serie (de hecho, este método devuelve un dataframe). Esta información incluye el número de muestras, el valor medio, la desviación estándar, el valor mínimo, máximo, la mediana y los valores correspondientes a los percentiles 25% y 75%.\n",
    "\n",
    "Siguiendo con el ejemplo visto en la sección anterior:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "almacén.describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El método acepta el parámetro percentiles conteniendo una lista (o semejante) de los percentiles a mostrar. También acepta los parámetros include y exclude para especificar los tipos de las características a incluir o excluir del resultado."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El método info\n",
    "\n",
    "El método info muestra un resumen de un dataframe, incluyendo información sobre el tipo de los índices de filas y columnas, los valores no nulos y la memoria usada: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "almacén.info()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Solo los dataframes tienen implementado este método."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El método value_counts\n",
    "\n",
    "Un método de las series pandas extremadamente útil es pandas.Series.value_counts. Este método devuelve una estructura conteniendo los valores presentes en la serie y el número de ocurrencias de cada uno. Estos valores se muestran en orden decreciente:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "s = pd.Series([3, 1, 2, 1, 1, 4, 1, 2, np.nan])\n",
    "print(s)\n",
    "s.value_counts()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Como puede apreciarse, por defecto no se incluyen los valores nulos. Este comportamiento puede modificarse haciendo uso del parámetro **dropna**:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.value_counts(dropna = False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En lugar de devolver los valores distintos y el número de ocurrencias, este método también puede agrupar los datos en \"bins\" y devolver una lista de bins (indicando sus márgenes) con el número de valores en cada uno de ellos. Por ejemplo, si quisiéramos agrupar los valores de la serie anterior en dos bins podríamos hacerlo de la siguiente forma:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.value_counts(bins = 2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Vemos que se han creados los dos bins, el primero conteniendo los valores entre 0.996 y 2.5 (intervalo abierto por la izquierda y cerrado por la derecha), bin en el que hay 6 valores, y el segundo conteniendo los valores entre 2.5 y 4 (intervalo también abierto por la izquierda y cerrado por la derecha), bin en el que hay 2 valores."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# -------------------------------------  Selección de información ----------------------------------\n",
    "\n",
    "# - - Selección de datos en Series\n",
    "\n",
    "Ya se ha comentado que una serie pandas consta de un array de datos y un array de etiquetas (el índice o index). Si al crear la serie no se ha especificado el índice, ya sabemos que se asignará uno implícito por defecto:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "s = pd.Series([10, 20, 30, 40])\n",
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podemos seleccionar los valores haciendo referencia al índice asignado con la misma notación que en un diccionario (la llamada \"notación corchetes\" o \"square bracket notation\"):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(s[0])\n",
    "print(s[2])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Usando esta sintaxis, si no se ha especificado un índice explícito, los índices negativos no están permitidos.\n",
    "\n",
    "Si se asignan índices de forma explícita:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([10, 20, 30, 40], index = [\"a\", \"b\", \"c\", \"d\"])\n",
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "podemos seleccionar los elementos usando el índice explícito o el implícito:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(s[\"a\"],s[0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(s[\"d\"],s[3])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con esta sintaxis, sí está permitido hacer uso de índices negativos para referirnos a los elementos desde el final de la estructura.\n",
    "\n",
    "Si los índices asignados son números enteros (al igual que las etiquetas del índice implícito), el índice implícito queda desactivado:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([10, 20, 30, 40], index = [3, 2, 1, 0])\n",
    "s"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Uso de rangos\n",
    "\n",
    "Es posible seleccionar rangos de valores. De esta forma, si usamos un rango numérico en una serie en la que hemos definido un índice explícito:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([10, 20, 30, 40], index = [\"a\", \"b\", \"c\", \"d\"])\n",
    "s"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[1:3]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "observamos que el rango se interpreta como haciendo referencia al índice implícito, y se incluyen los valores desde el primer índice incluido, hasta el último sin incluir.\n",
    "\n",
    "Si no se incluye alguno de los límites, el comportamiento es el estándar en Python (si no se incluye el primer valor, se consideran todos los elementos desde el principio, y si no se incluye el último valor, se consideran todos los elementos hasta el final):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[1:]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[:3]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si se utilizan los índices explícitos en el rango, el comportamiento es ligeramente diferente:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[\"a\":\"c\"]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[:\"c\"]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[\"b\":]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una posible fuente de confusión viene derivada del hecho de que, usando rangos, es posible hacer referencia tanto a las etiquetas como a los índices numéricos: si utilizamos etiquetas, hacemos referencia a las etiquetas (por supuesto):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([10, 20, 30, 40], index = [\"a\", \"b\", \"c\", \"d\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[\"b\":\"c\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "y, por tanto, si utilizamos números, hacemos referencia a los índices numéricos (¿por supuesto...?):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[1:3]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "¿Y qué ocurre si nuestras etiquetas son números? Pues que siempre que usemos rangos con números estaremos haciendo referencia a los índices numéricos: no es posible hacer referencia a las etiquetas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([10, 20, 30, 40], index = [3, 2, 1, 0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[1:3]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sin embargo, algo como"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[1]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "devolverá el valor cuya etiqueta es 1 (si existe):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([10, 20, 30, 40], index = [3, 2, 1, 0])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "o el valor cuya posición es 1 si dicha etiqueta no existe y el índice explícito no es numérico:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([10, 20, 30, 40], index = [\"a\", \"b\", \"c\", \"d\"])\n",
    "s[1]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Es debido a esto que existen los métodos loc e iloc que veremos poco más adelante. Dichos métodos hacen una referencia explícita a etiquetas o posiciones, respectivamente, eliminando cualquier duda al respecto de su interpretación."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Al igual que con los array NumPy, es posible indicar, no un elemento simple ni un rango, sino una lista de valores. Por ejemplo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[[3, 1]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este ejemplo, la lista contiene los números 3 y 1, y son los valores correspondientes a estos índices -y en el orden especificado- los devueltos por la instrucción.\n",
    "\n",
    "El resultado devuelto sigue siendo una serie pandas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "type(s[[3, 1]])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con esta notación, en el caso de que la serie tenga un índice explícito numérico, los valores de la lista se interpretan como haciendo referencia al índice explícito."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "También se puede utilizar el metodo **get**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.get(1) "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.get(\"b\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Método loc \n",
    "\n",
    "El método pandas.Series.loc permite seleccionar un grupo de elementos por etiquetas.\n",
    "\n",
    "Como argumento de este método puede utilizarse una única etiqueta:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([10, 20, 30, 40], index = [\"a\", \"b\", \"c\", \"d\"])\n",
    "s"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.loc[\"b\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este caso el argumento se interpreta siempre como etiqueta del índice, nunca como posición en dicho índice aun cuando se pase un número entero que no pertenece al conjunto de etiquetas y pueda representar una posición válida:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.loc[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "También podemos pasar al método una lista de etiquetas, en cuyo caso se extraen los valores correspondientes a dichas etiquetas y en el orden en el que se incluyen en la lista:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.loc[[\"d\", \"a\"]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Otra opción es pasar al método un rango:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[\"b\":\"d\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este caso es importante recalcar que, tal y como se ve en la imagen anterior, el método va a devolver todos los elementos entre los límites indicados ambos incluidos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Método iloc\n",
    "\n",
    "El método pandas.Series.iloc permite extraer datos de la serie a partir de los índices implícitos que éstos tienen asignados.\n",
    "\n",
    "La opción más simple es utilizar como argumento un simple número entero (el primer elemento de la serie recibe el índice cero):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([10, 20, 30, 40], index = [\"a\", \"b\", \"c\", \"d\"])\n",
    "s"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.iloc[1]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.iloc[0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.iloc[3]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si el número es negativo, hace referencia al final de la serie (en este caso, el último elemento recibe el índice -1) -y esto tanto si se ha especificado un índice explícito como si no-:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.iloc[-1]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.iloc[-4]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una segunda opción es pasar como argumento una lista o array de números, en cuyo caso se devuelven los elementos que ocupan dichas posiciones en el orden indicado en la lista o array:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.iloc[[2, 0]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "También podemos incluir en esta lista números negativos, con la funcionalidad ya comentada:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.iloc[[-2, 0]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una tercera opción es usar como argumento un rango de números:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.iloc[1:3]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Como vemos en el ejemplo anterior, si el rango tiene la forma a:b, se incluyen todos los elementos desde aquel cuyo índice es a (incluido) hasta el que tiene el índice b (sin incluir).\n",
    "\n",
    "Si no se especifica el primer valor, se consideran todos los elementos desde el principio de la serie:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.iloc[:3]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y, si no se especifica el segundo valor, se consideran todos los elementos hasta el final de la serie:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.iloc[2:]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "También pueden usarse valores negativos para indicar el comienzo y/o el final del rango:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.iloc[1:-1]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Uso de arrays booleanos\n",
    "\n",
    "Una muy interesante opción para seleccionar elementos de una serie pandas es usar arrays booleanos. Por ejemplo, partimos de la siguiente serie:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([5, 2, -3, 7, 8, 4])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podemos seleccionar un conjunto de valores de la misma haciendo referencia al nombre de la serie y, entre los corchetes, una lista o array de booleanos (también puede ser una serie de booleanos, como veremos un poco más adelante):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[[True, False, False, True, True, False]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este caso hemos seleccionado los elementos cuyos índices son 0, 3 y 4, que son los índices que ocupan los booleanos True en la lista de booleanos usada (lista cuya longitud deberá ser igual a la longitud de la serie pues, de no ser así, se devuelve un error).\n",
    "\n",
    "Esta lista o array de booleanos no tiene porqué ser especificada de forma explícita, puede ser el resultado de una expresión:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(type(s > 2))\n",
    "s > 2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí, hemos usado la expresión s > 2 para generar una serie pandas de booleanos, serie en la que los valores toman el valor True cuando el valor con el mismo índice de s toma un valor mayor estricto que 2.\n",
    "\n",
    "Podemos entonces usar este resultado para extraer valores de la serie s (valores que serán aquellos mayores que 2):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[s>2]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este mismo enfoque puede ser usado con los métodos pandas.Series.loc y pandas.Series.iloc ya vistos en las secciones anteriores con algún matiz adicional:\n",
    "\n",
    "El método loc puede ser usado tanto con un array explícito de booleanos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.loc[[True, False, False, True, True, True]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "como con una expresión que genera, por ejemplo, una serie pandas de booleanos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.loc[s>2]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sin embargo, el método iloc tiene un comportamiento ligeramente diferente. Puede ser usados con arrays explícitos de booleanos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.iloc[[True, False, False, True, True, True]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "pero el uso de expresiones que generen una serie pandas de booleanos devuelve un error:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.iloc[s>2]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si el objeto que está generando la estructura de booleanos (s, en s > 2) fuese un array NumPy en lugar de tratarse de una serie pandas, sí sería posible usar el método .iloc. De esta forma, la expresión s > 2 genera, como hemos visto, una serie pandas, pero podemos extraer los valores con el atributo values, que genera un array numpy:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "type((s>2).values)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "(s>2).values"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si usamos esta expresión para realizar la selección en la serie original s, el resultado es ahora el correcto:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.iloc[(s>2).values]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Es por ello que pandas recomienda usar el método loc cuando trabajemos con selección basada en booleanos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Selección aleatoria\n",
    "\n",
    "También podemos realizar una selección aleatoria a partir de una serie. El método pandas.Series.sample permite especificar o bien el número de elementos a extraer o bien la fracción del número total de elementos a extraer (parámetros **n** y **frac**, respectivamente), pudiendo especificar si la extracción se realiza con reemplazo o no (parámetro **replace**), los pesos a aplicar a cada elemento para realizar una extracción aleatoria ponderada (parámetro **weights**), y una semilla para el generador de números aleatorios que asegure la reproducibilidad de la extracción (parámetro **random_state**). Por ejemplo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([10, 20, 30, 40], index = [\"a\", \"b\", \"c\", \"s\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.sample(3, random_state = 18)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Hemos extraído 3 elementos, por defecto sin reemplazo, aplicando el valor 18 como semilla del generador de números aleatorios."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.sample(frac = 0.6, random_state = 18)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este otro ejemplo hemos extraído el 60% de los valores de la serie original haciendo uso del parámetro frac.\n",
    "\n",
    "Si no hay reemplazo, el número máximo de elementos que podemos extraer coincide con la longitud de la serie. Pero si la extracción la realizamos con reemplazo, podemos especificar cualquier número de elementos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.sample(10, random_state = 18, replace = True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Método pop\n",
    "\n",
    "El método pandas.Series.pop extrae y elimina un elemento de una serie cuyo índice se indica como argumento ...devolviendo un error en caso de que no exista::"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([1, 2, 3, 4])\n",
    "s"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.pop(1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([1, 2, 3, 4])\n",
    "try:\n",
    "    s.pop(18)\n",
    "except:\n",
    "    print(\"Error\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si la serie tiene un índice explícito, el argumento de pop hará referencia a este índice:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([10, 20, 30, 40], index = [\"a\", \"b\", \"c\", \"d\"])\n",
    "s"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.pop(\"a\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "y no al implícito, lo que devolvería un error:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    s.pop()\n",
    "except:\n",
    "    print(\"Error\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# - - Selección de datos en DataFrames\n",
    "\n",
    "Desde un punto de vista semántico, un dataframe puede ser considerado semejante a un diccionario de series, en el que las claves son los nombres de las columnas y los valores, las columnas (que son series pandas). En este ejemplo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas = pd.DataFrame({\n",
    "    \"Entradas\":[41, 32, 56, 18],\n",
    "    \"Salidas\":[17, 54, 6, 78],\n",
    "    \"Valoración\": [66, 54, 49, 66],\n",
    "    \"Límite\": [\"No\", \"Si\", \"No\", \"No\"],\n",
    "    \"Cambio\": [1.43, 1.16, -0.67, 0.77]},\n",
    "    index = [\"Ene\", \"Feb\", \"Mar\", \"Abr\"])\n",
    "ventas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podemos utilizar la sintaxis de los diccionarios para seleccionar la columna Entradas. Puede verse en el ejemplo siguiente cómo dicha columna es extraída con tipo de serie pandas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(type(ventas[\"Entradas\"]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas[\"Entradas\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esto significa que podemos realizar una selección en dicho resultado para, por ejemplo, extraer el valor correspondiente a febrero:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas[\"Entradas\"][\"Feb\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sin embargo, la más que razonable opción de eliminar los corchetes que separan ambos índices y sustituirlos por una coma no funciona:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas[\"Entradas\",\"Feb\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si, una vez seleccionada una columna, le asignamos una lista o array (o serie) de valores de la misma longitud, estamos modificando dicha columna del dataframe:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas[\"Entradas\"] = [33, 25, 40, 12]\n",
    "ventas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si asignamos un único valor escalar, este se propaga por toda la columna:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas[\"Salidas\"]=1\n",
    "ventas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si estuviésemos asignando un array cuya longitud no coincidiese con la de la columna (y no estuviésemos asignando un escalar), obtendríamos un error."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si asignamos una serie pandas se consideran los índices del dataframe y de la serie, haciendo coincidir los valores cuyos índices sean los mismos en ambas estructuras (si dicha columna no existe, se crea). En el caso de que haya valores en la serie con índices que no se encuentren en el dataframe, se descartan. Y en el caso de que haya índices en el dataframe que no se encuentren en la serie, se asigna un valor NaN."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Así, en el siguiente ejemplo, estamos añadiendo una serie cuyos índices son \"Ene\", \"Mar\", \"Abr\" y \"May\". Es decir, la serie no tiene un valor para el índice \"Feb\" que sí se encuentra en el dataframe (se asigna un NaN), e incluye el índice \"May\" que no se encuentra en el dataframe y se descarta:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas[\"Pérdidas\"] = pd.Series([5, 4, 6, 8], index = [\"Ene\", \"Mar\", \"Abr\", \"May\"])\n",
    "ventas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los valores asignados pueden proceder del propio dataframe:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas[\"Ganancias\"] = (ventas[\"Entradas\"]*2) - (ventas[\"Valoración\"]/10)\n",
    "ventas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "También podemos acceder a una columna con la llamada \"notación punto\":"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas.Ganancias"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Deberemos tener en cuenta que con esta notación no es posible crear nuevas columnas ni eliminarlas con la función del y que solo funcionará si el nombre de la columna no incluye espacios en blanco y no coincide con ninguna palabra reservada de Python."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Uso de rangos\n",
    "\n",
    "El uso de un rango numérico entre los corchetes realiza una selección de filas, lo que puede parecer una cierta incoherencia:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "df = pd.DataFrame(np.arange(18).reshape([6, 3]),\n",
    "                  index = [\"a\", \"b\", \"c\", \"d\", \"d\", \"f\"],\n",
    "                  columns = [\"A\", \"B\", \"C\"])\n",
    "df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df[2:5]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El equipo de pandas lo justifica diciendo que esta sintaxis resulta extremadamente conveniente al tratarse de un tipo de selección frecuentemente usada. Esto es cierto, pero el hecho de que selecciones aparentemente semejantes (df[1,2], df[[1, 2]], df[1:3, 5], etc.) devuelvan un error no facilita su comprensión.\n",
    "\n",
    "En todo caso, vemos en el ejemplo anterior que se devuelven las filas entre el primer valor del rango (incluido) y el último (sin incluir). También podríamos haber usado las etiquetas del índice:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df[\"b\":\"d\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "aunque en este caso la selección incluye tanto la fila correspondiente a la primera etiqueta como la fila correspondiente a la segunda."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df[\"b\":\"d\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "También podemos obviar la inclusión del primer o del segundo valor, considerándose las filas desde el comienzo/hasta el final del dataframe:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df[:3]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df[:\"c\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si, al realizar la selección, situamos entre los corchetes una lista de etiquetas, estaremos seleccionando columnas en el orden en el que aparecen en la lista y con formato dataframe:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(18).reshape([6, 3]),\n",
    "                  index = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"],\n",
    "                  columns = [\"A\", \"B\", \"C\"])\n",
    "df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(type(df[[\"C\", \"A\"]]))\n",
    "df[[\"C\",\"A\"]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "También es posible extraer de forma segura una columna de un dataframe usando el método pandas.DataFrame.get. Éste extrae la columna indicada devolviendo un valor alternativo (por defecto None) si dicha columna no existe:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(18).reshape([6, 3]),\n",
    "                  index = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"],\n",
    "                  columns = [\"A\", \"B\", \"C\"])\n",
    "df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.get(\"A\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.get(\"D\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El método LOC\n",
    "\n",
    "Al igual que ocurre con las series, el método pandas.DataFrame.loc permite seleccionar un conjunto de filas y columnas por etiquetas. Este método acepta diferentes argumentos. Para probarlos, partamos del siguiente dataframe:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(18).reshape([6, 3]),\n",
    "                  index = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"],\n",
    "                  columns = [\"A\", \"B\", \"C\"])\n",
    "df"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Uso con etiqueta simple\n",
    "\n",
    "El primer escenario lo encontramos cuando usamos este método indicando una única etiqueta. En este caso estamos seleccionando la fila cuya etiqueta se indique:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.loc[\"c\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El resultado es una serie pandas con las etiquetas de columnas del dataframe original como índice.\n",
    "\n",
    "Es necesario mencionar que el argumento será siempre interpretado como etiqueta, aun cuando pueda estar representando un índice válido: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(12).reshape([4, 3]),\n",
    "                  index = [1, 3, 0, 4],\n",
    "                  columns = [\"A\", \"B\", \"C\"])\n",
    "df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.loc[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Por supuesto, si dicha etiqueta no existe, se devuelve un error (nuevamente, aun cuando la etiqueta sea un número que pueda estar representando un índice válido.)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si pasamos a loc una lista de etiquetas, estaremos extrayendo las filas cuyas etiquetas se indican, y en el orden en el que aparezcan en la lista:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(18).reshape([6, 3]),\n",
    "                  index = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"],\n",
    "                  columns = [\"A\", \"B\", \"C\"])\n",
    "df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.loc[[\"c\", \"a\", \"e\"]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Al contrario de lo que ocurre cuando solo indicamos una etiqueta, el resultado es un dataframe. Y lo es aún cuando la lista contenga un único elemento:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(type(df.loc[[\"c\"]]))\n",
    "df.loc[[\"c\"]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Otra opción es utilizar rangos limitados por etiquetas. De esta forma, si continuamos con el mismo ejemplo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.loc[\"b\":\"d\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Obsérvese que la selección incluye todas las filas incluyendo las dos de los extremos del rango."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Extracción de filas y columnas\n",
    "\n",
    "En los ejemplos vistos hasta ahora estamos extrayendo una o varias filas para todas las columnas. En posible, por supuesto, especificar qué filas y qué columnas exactas queremos extraer. Así, si utilizamos una única etiqueta para indicar la fila, y una única etiqueta para indicar la columna, separadas por una coma, estaremos extrayendo un único valor:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.loc[\"a\", \"C\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podemos sustituir una de las dos etiquetas por el símbolo de dos puntos (:), lo que supondrá seleccionar todos los elementos de ese eje:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.loc[:, \"A\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esto supone que, por ejemplo, las dos expresiones siguientes devuelven el mismo resultado:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.loc[\"b\"]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.loc[\"b\", :]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los métodos vistos pueden combinarse. Podemos, por ejemplo, seleccionar la intersección de las filas e y c (en este orden) y la columna B:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.loc[[\"e\", \"c\"], \"B\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El método iloc\n",
    "\n",
    "El método pandas.DataFrame.iloc permite realizar selecciones por posición. Tal y como cabría esperar, pueden utilizarse diferentes tipos de argumentos que determinan qué elementos se están extrayendo.\n",
    "\n",
    "En este primer caso cuando hacemos uso un número entero estamos seleccionando la fila cuyo índice se indica:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.random.randint(0, 10, 18).reshape([6, 3]), \n",
    "               index = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"],\n",
    "               columns = [\"A\", \"B\", \"C\"])\n",
    "df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.iloc[2]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El número indicado siempre será tratado como posición, y no como etiqueta:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(12).reshape([4, 3]),\n",
    "                 index = [3, 2, 1, 0],\n",
    "                 columns = [\"A\", \"B\", \"C\"])\n",
    "df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.iloc[3]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Si el número es negativo, hace referencia al final del dataframe. Así, siguiendo con este último ejemplo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.iloc[-1]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si utilizamos como argumento una lista o array de números, estamos extrayendo las filas cuyos índices son los elementos del mismo, y en el orden en el que aparecen en él:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.random.randint(0, 10, 18).reshape([6, 3]), \n",
    "               index = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"],\n",
    "               columns = [\"A\", \"B\", \"C\"])\n",
    "df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.iloc[[3, 1]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el ejemplo anterior, estamos extrayendo las filas cuyos índices son 3 y 1, y extrayéndolas en este mismo orden.\n",
    "\n",
    "Si alguno de los índices es negativo, hará referencia al final de la lista."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si utilizamos un rango de números, como en el siguiente ejemplo en el que indicamos como argumento 2:4, estamos extrayendo las filas cuyos índices van de la primera cifra del rango incluida (2 en el ejemplo) hasta la última cifra sin incluir (4 en el ejemplo):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.iloc[2:4]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Como suele ser habitual, si no se especifica el primer valor, se consideran las filas desde la primera. Y si no se especifica el último valor, se consideran las filas hasta la última (incluida):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.iloc[:3]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.iloc[4:]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "También pueden usarse valores negativos para especificar el comienzo o el final del rango."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Extracción de filas y columnas\n",
    "\n",
    "Si añadimos un segundo argumento, estaremos haciendo referencia al índice de columna:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.iloc[3, 1]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el ejemplo anterior estamos extrayendo el valor correspondiente a la fila cuyo índice es 3 y a la columna cuyo índice es 1."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#  Selección con índices y etiquetas simultáneamente\n",
    "\n",
    "En ocasiones nos encontraremos con que resultaría de utilidad poder realizar selecciones mezclando etiquetas e índices, y los métodos vistos, loc e iloc, solo permiten el uso de etiquetas o de índices, respectivamente. Para poder mezclar ambos tipos de referencias podemos recurrir a los métodos pandas.Index.get_loc y pandas.Index.get_indexer, métodos asociados a los índices de un dataframe:\n",
    "\n",
    "El primero, get_loc, devuelve el índice de la etiqueta que se adjunte como parámetro. El segundo, get_indexer, devuelve un array con los índices de las etiquetas que se adjunten en forma de lista como parámetro. Por ejemplo, partimos del siguiente dataframe:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(18).reshape([6, 3]),\n",
    "                 index = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"],\n",
    "                 columns = [\"A\", \"B\", \"C\"])\n",
    "df"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si aplicamos los métodos comentados al índice de columnas haciendo referencia a etiquetas de columnas, obtenemos los siguientes resultados:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.columns.get_loc(\"B\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.columns.get_indexer([\"A\", \"C\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el primer caso hemos pasado la etiqueta \"B\" y el método ha devuelto su índice (1). En el segundo caso hemos pasado una lista de etiquetas y hemos obtenido un array con sus índices.\n",
    "\n",
    "Si ejecutamos estos métodos en el índice de filas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.index.get_loc(\"d\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.index.get_indexer([\"c\", \"e\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "obtenemos resultados semejantes.\n",
    "\n",
    "Ahora que sabemos cómo convertir etiquetas en sus índices equivalentes, podemos seleccionar datos de un dataframe mezclando etiquetas e índices si convertimos las etiquetas y utilizamos el método iloc ya visto. Por ejemplo, si quisiéramos extraer del anterior dataframe el dato que ocupa la fila \"c\" y la columna de índice 2, podríamos conseguirlo del siguiente modo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.iloc[df.index.get_loc(\"c\"), 2]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "O si deseásemos obtener de las filas 5 y 3 (en este orden) los valores correspondientes a las columnas C y A (en este orden), podríamos hacerlo con la siguiente expresión:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.iloc[[5, 3], df.columns.get_indexer([\"C\", \"A\"])]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Uso de listas de booleanos\n",
    "\n",
    "Otro método especialmente útil para la selección es el uso de listas de booleanos. Nuevamente puede parecer un tanto incoherente aunque, en este caso, su uso sí es extremadamente conveniente. Veamos por qué:\n",
    "\n",
    "Si partimos del mismo dataframe usado en la sección anterior, podemos crear una lista de booleanos (que, por motivos puramente pedagógicos, asignamos a una variable, mask) y realizar la selección con ella entre los corchetes. Vemos a continuación que este método también selecciona filas del dataframe:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(18).reshape([6, 3]),\n",
    "                 index = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"],\n",
    "                 columns = [\"A\", \"B\", \"C\"])\n",
    "df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mask = [True, False, True, False, False, True]\n",
    "df[mask]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El vector de booleanos deberá tener la misma longitud que el índice de filas (es decir, un booleano por fila) y la selección devolverá aquellas filas para las que el elemento correspondiente del vector tome el valor True.\n",
    "\n",
    "La verdadera potencia de este estilo de selección se pone de manifiesto cuando la máscara se genera a partir de los datos del propio dataframe. Por ejemplo, si queremos seleccionar las filas para las que el valor de la columna A sea mayor que 7:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df[df.A > 7]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este tipo de filtrados resultan muy frecuentes en entornos de análisis, de ahí que la posibilidad de realizarlos sin necesidad de recurrir a métodos adicionales (loc, iloc o get, por ejemplo) resulte tan conveniente.\n",
    "\n",
    "Aun así, esta técnica también es compatible con los métodos loc e iloc, con algún matiz adicional:\n",
    "\n",
    "Con loc podemos usar directamente una expresión de comparación como la vista:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.loc[df.B > 6]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sin embargo, con iloc nos veremos obligados a extraer los valores del dataframe resultante de la comparación -tal y como ocurría con las series- pues, de otro modo, obtendremos un error:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.iloc[(df.B > 6 ).values]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Evitamos problemas si, tal y como sugiere pandas, utilizamos siempre el método loc."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Selección aleatoria\n",
    "\n",
    "Al igual que ocurre con las series, también los dataframes tienen un método que permite extraer elementos del mismo de forma aleatoria: pandas.DataFrame.sample. Este método permite especificar el número de elementos a extraer (o el porcentaje respecto del total, parámetros **n** y **frac**, respectivamente), si la extracción se realiza con reemplazo o no (parámetro **replace**), los pesos a aplicar a los elementos para realizar una extracción aleatoria ponderada (parámetro **weights**) y una semilla para el generador de números aleatorios que asegure la reproducibilidad de la extracción (parámetro **random_state**). También es posible indicar el eje a lo largo del cual se desea realizar la extracción (por defecto se extraen filas, correspondiente al eje 0).\n",
    "\n",
    "Veamos un ejemplo. Si partimos del siguiente dataframe:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(18).reshape([6, 3]),\n",
    "                  index = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"],\n",
    "                  columns = [\"A\", \"B\", \"C\"])\n",
    "df"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "podemos extraer 3 filas de forma aleatoria, sin reemplazo (opción por defecto) y fijando como semilla del generador de números aleatorios el número 18, de la siguiente forma:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.sample(3, random_state = 18)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si especificamos como eje el valor 1, estaremos extrayendo columnas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.sample(2, random_state = 18, axis = 1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si hacemos uso del parámetro frac, podemos especificar el porcentaje de elementos a extraer:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.sample(frac = 0.6, random_state = 18)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El método pop\n",
    "\n",
    "Otra forma de extraer datos es la proporcionada por el método pandas.DataFrame.pop, que extrae y elimina una columna de un dataframe:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(15).reshape([3, 5]), \n",
    "                  index = [\"a\", \"b\", \"c\"],\n",
    "                  columns = [\"A\", \"B\", \"C\", \"D\", \"E\"])\n",
    "df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "columna = df.pop(\"B\")\n",
    "columna"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Resumen de tipos de selección \n",
    "\n",
    "La cantidad de posibilidades que nos ofrece la librería pandas a la hora de extraer información de un dataframe puede resultar un tanto abrumadora. Veamos un resumen de las principales técnicas vistas y qué estamos extrayendo con cada una. En la siguiente lista e representa una etiqueta, i un índice y b un booleano:\n",
    "\n",
    "<img src=\"https://i.imgur.com/B0BpStV.png\">\n",
    "\n",
    "No se incluyen en el listado anterior combinaciones de estos métodos ni los métodos para la selección usando índices y etiquetas simultáneamente.\n",
    "\n",
    "Aunque, a primera vista, pueda parecer bastante confuso, es posible destacar algunas reglas básicas:\n",
    "\n",
    "1. Cuando se usan los métodos loc o iloc, el primer argumento siempre hace referencia a filas y el segundo a columnas. Esto significa que si no se incluye el segundo argumento, siempre estaremos extrayendo filas.\n",
    "2. El método get devuelve columnas.\n",
    "3. Sin incluir los métodos loc, iloc y get, solo hay -en el listado anterior- dos formas de extraer columnas: usando como argumento una etiqueta y usando como argumento una lista de etiquetas.\n",
    "4. Del punto anterior extraemos como corolario que cualquier otra notación va a devolver filas (el uso de rangos y el uso de listas de booleanos)\n",
    "\n",
    "Y, de hecho, si de los cuatro puntos anteriores quitamos los dos primeros por obvios, nos quedan dos reglas muy simples:\n",
    "\n",
    "1. El uso de una etiqueta o de una lista de etiquetas devuelve columnas\n",
    "2. En otros casos se devuelven filas (rangos de números o de etiquetas, o listas de booleanos)\n",
    "\n",
    "Y, al respecto del tipo de estructura devuelta (si es un escalar, una serie o un dataframe) también es posible identificar una regla: salvo el caso más obvio en el que estemos extrayendo un valor resultante de la intersección de una fila y una columna, si la nomenclatura que estamos usado permite extraer más de una fila o de una columna (aunque estemos extrayendo solo una en un momento dado) devolverá siempre dataframes y, en caso contrario, series.\n",
    "\n",
    "Por ejemplo, el uso de rangos permite extraer más de una fila (o de una columna), de forma que su uso siempre devuelve un dataframe. Así, df[\"e1\":\"e2\"] siempre devolverá un dataframe, aunque en este ejemplo estemos usando el rango para extraer una única columna.\n",
    "\n",
    "Otro ejemplo: si estamos usando la notación df.loc[:, \"e\"], estamos extrayendo una columna y solo una columna, y con esta notación (dos puntos para las filas y una etiqueta para columnas) nunca podríamos extraer más de una columna, de forma que el resultado de la extracción siempre será una serie."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Edición de series \n",
    "\n",
    "Podemos modificar un valor de una serie usando la notación corchetes, y haciendo referencia a índices o a etiquetas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([1, 2, 3, 4, 5], index = [\"a\", \"b\", \"c\", \"d\", \"e\"])\n",
    "s"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[0] = -1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[\"b\"] = -2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podemos asignar un valor a un rango, definido éste por índices o por etiquetas, asignándose dicho valor a cada uno de los elementos involucrados en el rango:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[1:3] = 0\n",
    "s"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[\"b\":\"d\"] = -2 \n",
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Como ya hemos visto en más de una ocasión, si el rango está delimitado por números (haciendo referencia a la posición de los elementos), el último elemento del rango no se incluye. Por el contrario, si está delimitado por etiquetas, el último elemento sí se incluye.\n",
    "\n",
    "Al rango podemos asignar también una lista de valores, aunque en este caso la lista deberá tener el mismo número de elementos que el rango en cuestión:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[1:3] = [0, 1] \n",
    "s"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[\"b\":\"d\"] = [10, 11, 12]\n",
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si asignamos un valor haciendo referencia a una etiqueta que no existe en el índice, se añade dicha etiqueta al índice y se le asigna el valor:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[\"f\"] = 0\n",
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esto solo funciona con etiquetas. Si utilizamos un índice y éste no existe en la serie, se devolverá un error.\n",
    "\n",
    "Si usamos un rango con etiquetas y alguna de las etiquetas no existe, solo se asigna el valor a las existentes:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[\"f\":\"h\"] = 0\n",
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Por último, también podemos usar en la selección una lista -tanto de índices como de etiquetas-, en cuyo caso ya sabemos que estamos seleccionando los valores indicados en el orden indicado. Por ejemplo, podemos usar la lista [\"c\", \"a\"] para asignar a los elementos correspondientes los valores 1 y 2, respectivamente:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[[\"c\", \"a\"]] = [1, 2]\n",
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si utilizamos índices, el resultado es semejante:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s[[1, 0]] = [20, 21]\n",
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Eliminación series método pop y drop\n",
    "\n",
    "El método pop ya ha sido presentado por eso se presentará el método pandas.Series.drop el cual devuelve una copia de la serie tras eliminar el elemento cuya etiqueta se especifica como argumento:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([1, 2, 3, 4, 5], \n",
    "              index = [\"a\", \"b\", \"c\", \"d\", \"e\"])\n",
    "s"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "r = s.drop(\"b\")\n",
    "r"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este ejemplo hemos pasado como único argumento la etiqueta del elemento a eliminar, y el método ha devuelto la serie sin dicho elemento. Si la etiqueta no se encontrase en la serie, se devolvería un error.\n",
    "\n",
    "También podemos pasar como argumento no una etiqueta, sino una lista de etiquetas. En este caso se eliminarán todos los elementos con dichas etiquetas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "r = s.drop([\"d\", \"a\"])\n",
    "r"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Obsérvese que las etiquetas no tienen que estar en orden.\n",
    "\n",
    "El argumento inplace = True realiza la eliminación inplace (modificando directamente la serie).\n",
    "\n",
    "Este método exige el uso de etiquetas para seleccionar los elementos a eliminar. Esto significa que si en un momento dado necesitamos eliminar uno o más elementos por su índice, deberemos convertirlos en sus correspondientes etiquetas, lo que resulta extremadamente sencillo seleccionando los elementos adecuados del index. En el siguiente ejemplo, partimos del mismo ejemplo ya visto anteriormente:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([1, 2, 3, 4, 5], index = [\"a\", \"b\", \"c\", \"d\", \"e\"])\n",
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si quisiéramos eliminar los elementos cuyos índices son 1 y 3, bastaría recordar que el atributo index devuelve todas las etiquetas y que s.index[[1, 3]] devuelve las correspondientes a dichos índices:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.index[[1, 3]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si pasamos esta expresión como argumento del método drop, obtendremos el resultado esperado:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.drop(s.index[[1, 3]])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Método Where\n",
    "\n",
    "El método pandas.Series.where permite filtrar los valores de una serie de forma que solo los que cumplan cierta condición se mantengan. Los valores que no la cumplan son sustituidos por un valor (NaN por defecto):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series(np.arange(0, 10))\n",
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Supongamos ahora que queremos filtrar los valores de s que sean pares:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.where(s % 2 == 0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Comprobamos que los valores que no cumplen la condición son sustituidos por NaN. Podemos modificar este valor de reemplazo pasando al método como segundo argumento el valor que queremos fijar:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.where(s % 2 == 0, -1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Edición de DataFrame\n",
    "\n",
    "Hemos visto la gran variedad de formas que tenemos a nuestra disposición para seleccionar elementos o bloques de elementos de un dataframe, y cada una de estas selecciones puede ser utilizada para modificar los valores contenidos en el dataframe. Veamos algunos ejemplos:\n",
    "\n",
    "Podemos modificar un valor concreto usando los métodos loc o iloc, en función de que queramos usar sus etiquetas o índices:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(12).reshape([4, 3]), \n",
    "                  index = [\"a\", \"b\", \"c\", \"d\"],\n",
    "                  columns = [\"A\", \"B\", \"C\"])\n",
    "df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.iloc[1, 2] = -1\n",
    "df"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podemos modificar una columna completa seleccionándola y asignándole, por ejemplo, una lista con los nuevos valores. Si partimos del mismo ejemplo que en el caso anterior..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(12).reshape([4, 3]), \n",
    "                  index = [\"a\", \"b\", \"c\", \"d\"],\n",
    "                  columns = [\"A\", \"B\", \"C\"])\n",
    "\n",
    "df[\"A\"] = [10, 20, 30, 40]\n",
    "df"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este caso, la longitud de la lista conteniendo los valores a insertar deberá coincidir con la longitud de la columna, salvo que en lugar de una lista se esté asignando un único valor, en cuyo caso se propagará a toda la columna.\n",
    "\n",
    "Si la selección es un bloque de datos de un tamaño arbitrario, nos encontramos en el mismo escenario: o bien insertamos datos con el mismo tamaño que la selección, o insertamos un único valor que se propagará a toda la selección. Veamos el primer caso:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(12).reshape([4, 3]), \n",
    "                  index = [\"a\", \"b\", \"c\", \"d\"],\n",
    "                  columns = [\"A\", \"B\", \"C\"])\n",
    "\n",
    "df.loc[\"b\":\"c\", \"A\":\"B\"] = [[-1, -2], [-3, -4]]\n",
    "df"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este ejemplo hemos seleccionado un bloque de 2x2, y hemos insertado datos con una estructura de las mismas dimensiones."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(12).reshape([4, 3]), \n",
    "                  index = [\"a\", \"b\", \"c\", \"d\"],\n",
    "                  columns = [\"A\", \"B\", \"C\"])\n",
    "\n",
    "df.loc[\"b\":\"c\", \"A\":\"B\"] = -1 \n",
    "df"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "y en este segundo caso hemos asignado un único valor a la misma selección.\n",
    "\n",
    "Nos hemos encontrado también con el caso de insertar datos en una columna o fila inexistente, en cuyo caso se crea y se le asignan los valores en cuestión. En el primer caso (de columna inexistente):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(12).reshape([4, 3]), \n",
    "                  index = [\"a\", \"b\", \"c\", \"d\"],\n",
    "                  columns = [\"A\", \"B\", \"C\"])\n",
    "\n",
    "df[\"D\"] = [10, 20, 30, 40] \n",
    "df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Y en el segundo (de fila inexistente):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(12).reshape([4, 3]), \n",
    "                  index = [\"a\", \"b\", \"c\", \"d\"],\n",
    "                  columns = [\"A\", \"B\", \"C\"])\n",
    "\n",
    "df.loc[\"e\"] = [10, 20, 30] \n",
    "df"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El método Where\n",
    "\n",
    "De forma semejante a las series, el método de los dataframes where filtra los valores contenidos en el dataframe de forma que solo los que cumplan cierta condición se mantengan. El resto de valores son sustituidos por un valor que, por defecto, es NaN.\n",
    "\n",
    "Por ejemplo, partimos del siguiente dataframe:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(12).reshape(-1, 3), columns=[\"A\", \"B\", \"C\"])\n",
    "df"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si ahora queremos filtrar los valores múltiplos de 2, por ejemplo, podemos hacerlo de la siguiente forma:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.where(df % 2 == 0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Todos aquellos valores que no son múltiplo de 2 son sustituidos por NaN. Si, por ejemplo, quisiéramos cambiar de signo a los valores que no cumplen la condición impuesta, lo haríamos así:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.where(df % 2 == 0, -df)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Eliminación de elementos \n",
    "\n",
    "El método pandas.DataFrame.drop elimina las filas o columnas indicadas y devuelve el resultado, permitiéndose diferentes criterios para especificarlas.\n",
    "\n",
    "El primer criterio consiste en indicar la lista de etiquetas a eliminar y el eje al que pertenecen. Partamos del siguiente dataframe:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(np.arange(16).reshape([4, 4]), \n",
    "                  index = [\"a\", \"b\", \"c\", \"d\"],\n",
    "                  columns = [\"A\", \"B\", \"C\", \"D\"])\n",
    "df"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podemos eliminar, por ejemplo, las filas cuyas etiquetas son \"a\" y \"c\" con el siguiente código:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.drop([\"a\", \"c\"], axis = 0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Obsérvese que lo que se muestra es el resultado de eliminar las filas indicadas del dataframe. Éste no se modifica salvo que utilicemos el argumento inplace = True.\n",
    "\n",
    "Como el eje por defecto es el 0, la instrucción anterior es equivalente a:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.drop([\"a\", \"c\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para eliminar columnas, habría que indicar el eje correspondiente:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.drop([\"B\", \"D\"], axis = 1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Otra alternativa para especificar si estamos eliminando filas o columnas es utilizar directamente los parámetros index y columns. Así, otra forma de eliminar las filas \"a\" y \"c\" sería la siguiente:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.drop(index = [\"a\", \"c\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "-el resultado es el mismo que antes, lógicamente-. Y para eliminar las columnas \"B\" y \"D\":"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.drop(columns = [\"B\", \"D\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Unión de series y dataframes\n",
    "\n",
    "Frecuentemente nos encontramos con que los datos a analizar están repartidos entre dos o más bloques de datos, lo que nos obliga a unirlos, bien concatenándolos, o bien realizando un \"join\" entre las estructuras (uniones del mismo tipo que las realizadas en bases de datos). Revisemos las funciones asociadas.\n",
    "\n",
    "# Unión de series\n",
    "\n",
    "Comencemos revisando las opciones disponibles para las series pandas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# La función concat\n",
    "\n",
    "Un caso con el que nos encontramos con relativa frecuencia es aquel en el que queremos unir una serie a otra. Por ejemplo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([1, 2, 3, 4, 5,], index = [\"a\", \"b\", \"c\", \"d\", \"e\"])\n",
    "r = pd.Series([10, 11, 12], index = [\"f\", \"g\", \"h\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si deseamos unir r y s en una nueva serie, podemos usar la función pandas.concat. Esta función permite especificar el eje a lo largo del cual unir los diferentes objetos (pueden ser series o dataframes). Por defecto, la concatenación se realiza a lo largo del eje 0:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = pd.concat([s, r])\n",
    "print(type(t))\n",
    "t"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podemos ver en el ejemplo anterior que el resultado es una serie pandas.\n",
    "\n",
    "Si especificamos como eje de concatenación el eje 1, pandas alineará los valores con idénticas etiquetas. En el siguiente ejemplo, las series a y b tienen algunas etiquetas comunes (y otras no). El resultado incluye todas las etiquetas asignando el valor NaN (\"Not a Number\") a aquellos valores desconocidos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "a = pd.Series([1, 2, 3, 4, 5,], index = [\"a\", \"b\", \"c\", \"d\", \"e\"])\n",
    "b = pd.Series([10, 11, 12], index = [\"a\", \"b\", \"f\"])\n",
    "pd.concat([a, b], axis = 1, sort = True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "(se ha utilizado el argumento sort = True para ocultar cierto aviso al respecto de un cambio en la funcionalidad de esta función en versiones futuras de la librería pandas)\n",
    "\n",
    "Como puede observarse, el resultado es un dataframe:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "type(pd.concat([a, b], axis = 1, sort = True))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Por otro lado, ya sabemos que las etiquetas del índice no tienen por qué ser diferentes, de forma que si estuviésemos concatenando series con etiquetas comunes en sus índices, el resultado sería equivalente a los vistos hasta ahora:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = pd.Series([1, 2, 3, 4], index = [\"a\", \"b\", \"c\", \"d\"])\n",
    "r = pd.Series([10, 11, 12], index = [\"a\", \"c\", \"e\"])\n",
    "pd.concat([s, r])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este ejemplo hemos concatenado dos series que tienen dos etiquetas comunes (\"a\" y \"c\"), y vemos que las dos apariciones de cada una de ellas se incluyen en el resultado de la concatenación."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El método append\n",
    "\n",
    "Otra alternativa es usar el método pandas.Series.append, versión simplificada de la función concat ya vista que devuelve la unión de la serie sobre la que se aplica con otra (u otras) series, pero solo a lo largo del eje 0. Veámoslo en funcionamiento:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "a = pd.Series([1, 2, 3, 4, 5], index = [\"a\", \"b\", \"c\", \"d\", \"e\"])\n",
    "b = pd.Series([10, 11, 12], index = [\"f\", \"g\", \"h\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "c = a.append(b)\n",
    "c"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si el argumento ignore_index toma el valor True, se ignoran las etiquetas de las series:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "c = a.append(b, ignore_index = True)\n",
    "c"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Concatenación y unión de dataframes\n",
    "\n",
    "Ésta es otra de las áreas en las que la variedad de opciones puede resultar confusa. A modo de resumen, digamos que pandas ofrece dos principales funciones con este objetivo: pandas.concat y pandas.merge.\n",
    "\n",
    "- La función concat permite concatenar dataframes a lo largo de un determinado eje\n",
    "- La función merge permite realizar uniones (joins) entre dataframes tal y como se realizan en bases de datos. Esta función también está disponible como método: pandas.DataFrame.merge\n",
    "\n",
    "Hay una tercera función que está disponible solo como método: pandas.DataFrame.append. El método append ofrece una funcionalidad semejante a la de la función concat pero reducida. Así, por ejemplo, solo permite realizar concatenaciones a lo largo del eje 0 (es decir, verticalmente).\n",
    "\n",
    "Veamos en las siguientes páginas algunos ejemplos de estas funciones."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# La función concat\n",
    "\n",
    "La función pandas.concat es la responsable de concatenar dos o más dataframes (y de todas las estructuras proveídas por pandas) a lo largo de un eje, con soporte a lógica de conjuntos a la hora de gestionar etiquetas en ejes no coincidentes. Veamos un primer caso, el más sencillo posible, para el que partimos de los siguientes dos dataframes:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "df1 = pd.DataFrame(np.arange(9).reshape([3, 3]),\n",
    "                  index = [\"a\", \"b\", \"d\"],\n",
    "                  columns = [\"A\", \"B\", \"C\"]) \n",
    "df1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df2 = pd.DataFrame(np.arange(12).reshape([4, 3]),\n",
    "                  index = [\"a\", \"b\", \"c\", \"e\"],\n",
    "                  columns = [\"B\", \"C\", \"D\"]) \n",
    "df2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nota: tal y como ha ocurrido en secciones anteriores, en algunos de los ejemplos que se muestran a continuación se utiliza el argumento sort = False para evitar que se muestre un aviso al respecto de cierto cambio de funcionalidad que se producirá en futuras versiones de esta librería.\n",
    "\n",
    "Si pasamos a la función concat ambos dataframes como primer argumento (en forma de lista), obtenemos el siguiente resultado:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.concat([df1, df2], sort = False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Vemos cómo, por defecto, la concatenación se ha realizado a lo largo del eje 0 (eje vertical), uniendo los índices de fila de ambos dataframes, y alineando las columnas por su etiqueta. Los valores para los que no hay datos se han rellenado con NaN (opción correspondiente al argumento por defecto join: \"outer\").\n",
    "\n",
    "Si especificamos que la concatenación se realice a lo largo del eje 1 (eje horizontal), el resultado es el siguiente:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.concat([df1, df2], axis = 1, sort = False) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De modo semejante al primer ejemplo, se han introducido NaN's allí donde no había datos, y se han alineado las filas por su etiqueta.\n",
    "\n",
    "Estos dos ejemplos vistos son tipo \"Outer\" (opción por defecto), considerando todas las etiquetas de los dos dataframes aun cuando no sean comunes a ambos. Pero si especificamos el argumento join = \"Inner\", los resultados pasan a considerar solo las etiquetas comunes. Así, para el primer ejemplo visto tenemos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.concat([df1, df2], join = \"inner\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Incluyendo solo las columnas B y C comunes a ambos dataframes. Y para el segundo ejemplo tenemos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.concat([df1, df2], axis = 1, join = \"inner\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "...incluyendo solo las filas a y b comunes a ambos dataframes.\n",
    "\n",
    "El parámetro ignore_index controla el índice a asignar al eje a lo largo del cuál se realiza la concatenación. Si este parámetro toma el valor False (por defecto), el eje de concatenación mantiene las etiquetas de los dataframes originales. Si toma el valor True, se ignoran dichas etiquetas y el resultado de la concatenación recibe un nuevo índice automático numérico. Por ejemplo, si añadimos al siguiente ejemplo el argumento ignore_index=True, obtenemos el siguiente resultado:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.concat([df1, df2], axis = 1, join = \"inner\", ignore_index = True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El método append\n",
    "\n",
    "Como se ha comentado, el método pandas.DataFrame.append es un atajo de la función concat que ofrece funcionalidad semejante pero limitada: no permite especificar el eje de concatenación (siempre es el eje 0) ni el tipo de \"join\" (siempre es tipo \"Outer\").\n",
    "\n",
    "Si seguimos con los mismos dataframes de la sección anterior:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df1 = pd.DataFrame(np.arange(9).reshape([3, 3]),\n",
    "                  index = [\"a\", \"b\", \"d\"],\n",
    "                  columns = [\"A\", \"B\", \"C\"]) \n",
    "df1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df2 = pd.DataFrame(np.arange(12).reshape([4, 3]),\n",
    "                  index = [\"a\", \"b\", \"c\", \"e\"],\n",
    "                  columns = [\"B\", \"C\", \"D\"]) \n",
    "df2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "...podemos ver cuál es el resultado de aplicar este método a df1:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df1.append(df2, sort = False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Al igual que ocurría con la función concat, el parámetro ignore_index nos permite controlar las etiquetas que recibe el índice del resultado: las de los dataframes originales (con ignore_index = False, opción por defecto), o uno nuevo automático (con ignore_index = True)."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
