Está en la página 1de 40

Capítulo 1: Módulo Numpy

1.1- Arrays, creación, redimensión, slicing y filtrado


1.2- Matrices y vectores con numpy
1.3- Tipos de datos, copias y vistas
1.4- Bucles en arrays
1.5- Concatenación de arrays
1.6- Dividiendo arrays
1.7- Búsquedas y ordenamientos en un array
1.8- Aleatoriedad en numpy
1.9- Funciones univesales
1.10- Redondeo y matemáticas en numpy
1.11- Conjuntos en Numpy
Capítulo 2: Módulo pandas y dataframes
2.1- Dataframes y su construcción
2.2- Dimensiones de los DataFrames
2.3- Extracción de columnas
2.4- Extracción de filas
2.5- Métodos de DataFrames
2.6- Bucles en dataframes
2.7- Ficheros CSV
2.8- Ficheros JSON
2.9- Datos faltantes
2.10- Filtrado de dataframes
2.11- Series de pandas
2.12- índice multinivel o Multiíndices de pandas
2.13- Tablas pivote o tablas dinámicas
2.14- Concatenación de dataframes
2.15- Conexión con bases de datos tipo SQL
2.16- Series de tiempo en pandas
Capítulo 3: Archivos importados en Python
3.1- Archivos txt
3.2- Ceando txt desde Python
3.3- Sobreescribiendo un txt
3.4- Cargando .CSV con open()

_________________________________________________________________________________________________________
____________________

Capítulo 1: Módulo Numpy

*****1.1- Arrays, creación, redimensión, slicing y filtrado*****

Numpy es una paquetería de métodos numéricos muy importante.

En esta paquetería podremos hacer uso de los arrays, son objetos del tipo ndarray (array n dimensional) y son listas que se
procesan mucho más rápido que una lista convencional, lo cual es importante en la ciencia de datos.

Una vez importada la librería numpy podemos revisar la versión instalada a través del código:
import numpy
print( numpy.__version__ )
duarante estas secciones importaremos numpy como np:
import numpy as np
Pra crear un ndarray usamos el método .array(). El contenido puede ser una lista, por ejemplo:
l = [ 1 , 2 , 3, 4 , 5 ]
a = np.array( l )
nota que al imprimir no tiene comas, lo cual lo distingue de las listas.

También se puede a partir de una tupla


l=(1,2,3,4,5)
b = np.array( l )

Se puede calcular las dimensiones de un array usamos .ndim() . Un array 0-dimensional es un número. Los 1-dimensionales son
vectores, los 2-dimensionales un array de arrays etc. Es fácil ver que .ndim es el número de entradas de la tupla que se
genera con el atributo .shape

Básicamente la dimensión de un array es la dimensión tensorial, es decir, el número de índices del tensor o en lenguaje menos
técnico, es el número de niveles de profundidad. Al momento de crear un array n-dimensional, los subarrays n-1 dimensionales
de éste deben tener todos la misma dimensión.
arreglo_dim0 = np.array( 77 )
arreglo_dim1 = np.array( [ 1 , 2 , 3 , 4 , 5 ] )
arreglo_dim2 = np.array( [ [ 1 , 2 , 3 , 4 ] , [ 5 , 6 , 7 , 8 ] ] )
arreglo_dim3 = np.array( [ [ [ 1 , 2 ] , [ 3 , 4 ] , [ 5 , 6 ] , [ 7 , 8 ] , [ 9 , 10 ] ] ,
[ [ 11 , 12 ] , [ 13 , 14 ] , [ 15 , 16 ] , [ 17 , 18 ] , [ 19 , 20 ] ] ,
[ [ 21 , 22 ] , [ 23 , 24 ] , [ 25 , 26 ] , [ 27 , 28 ] , [ 29 , 30 ] ] ] )

El atributo .shape nos devuelve una tupla con el número de elementos en cada dimensión, es decir, la cantidad de números
que puede tomar cada índice del tensor.

Nota que para calcular el número de entradas de un array basta que multipliquemos las entradas de la tupla que obtenemos con
.shape, tal como hicimos con las matrices. Nota también que len( arreglo.shape ) == arreglo.ndim

Para construir un arreglo de cierta profundidad a partir de una lista, a np.array() le podemos pasar el parámetro ndmin =
con el número de profundidad que necesitamos:
arreglo = np.array( [ 1 , 2 , 3 , 4 , 5 , 6 , 7 ] , ndmin = 7 )

Podemos construir un array a través de un iterable mediante np.arange() cuyos parámetros son los de un range usual, por
ejemplo:
np.arange( 0 , 100 , 2 )

Podemos construir arrays con ceros con np.zeros( n ):


np.zeros( 3 )
También tensores si le pasamos una tupla con las dimensiónes:
np.zeros( ( 4 , 5 ) )
Lo análogo hace el método np.ones( ) pero con unos en lugar de ceros.

Podemos crear un array unidimensional de n elementos ordenados e igualmente espaciados de manera ascendente con la
función
np.linspace( inicio , fin , cantidad ), por ejemplo, un array de 100 valores entre cero y uno con la siguiente línea:
valores = np.linspace( 0 , 1 , 100 )
es muy útil cuando queremos graficar una función, con un lineplot de matplotlib o seaborn, que veremos más delante.

Podemos crear la matríz(array de dimensión tensorial 2) identidad de dimensión n con el comando np.eye( n )
id_5 = np.eye( 5 )
Podemos agregar dimensiones extra en un array con la función np.expand_dims( array , axis = ), el axis de un array n
dimensional, tiene n+1 ejes, de 0 a n. Mientras que, si hay dimensiones que no se están usando, usamos la función
np.squeeze( array )

Podemos reordenar las entradas cambiando las dimensiones del tensor a través del método .reshape() en donde colocamos las
nuevas dimensiones del array. Para ello debaen coinicidir con el número de entradas originales.
arreglo = np.array( [ [ 1 , 2 , 3 , 4 ] , [ 5 , 6 , 7 , 8 ] ] ) #arreglo con forma ( 2 , 4 )
primera_ordenacion = arreglo.reshape( 4 , 2 ) #arreglo con forma ( 4 , 2 )
segunda_ordenacion = arreglo.reshape( 8 , 1 ) #arreglo con forma ( 8 , 1 )

Al aplicarle la propiedad .base a un array ya redimensionado, se consigue el array original.


segunda_ordenacion.base
es decir, el arreglo original no se borra de la reordenación.

Podemos pasar un -1 en un solo argumento de .reshape() lo cual significará que numpy calcula automáticamnte la dimensión que
falta en este -1.
tercera_ordenacion = arreglo.reshape( 2 , 2 , 2 , -1 )
solo se epuede colocar un -1 en una dimensión.

Este -1 puede ser muy útil para aplanar un narray de varias dimensiones un uno unidimensional pasándole solo -1 al método
.reshape(), por ejemplo:
nuevo_aplanado = arreglo.reshape( -1 )
otra forma es usar la función .flatten() para aplanar arrays:
nuevo_aplanado = arreglo.flatten()

La función reshape que acabamos de ver es un método que aplica sobre los arrays, sin embargo, numpy también la tiene
implementada por sí sola mediante el comando np.reshape( array, shape ):
arreglo = np.array( [ [ 1 , 2 , 3 , 4 ] , [ 5 , 6 , 7 , 8 ] ] ) #arreglo con forma ( 2 , 4 )
primera_ordenacion = np.reshape( arreglo , ( 4 , 2 ) ) #arreglo con forma ( 4 , 2 )
segunda_ordenacion = np.reshape( arreglo , (8 , 1 ) ) #arreglo con forma ( 8 , 1 )
además se le puede hacer mandar un tercer argumento, que determina el tipo de reshape optimizado en otro lenguaje, para el
lenguaje C se le manda "C", para fortran "F" y para la forma nativa del pc se le manda una "A":
arreglo = np.array( [ [ 1 , 2 , 3 , 4 ] , [ 5 , 6 , 7 , 8 ] ] ) #arreglo con forma ( 2 , 4 )
primera_ordenacion = np.reshape( arreglo , ( 4 , 2 ) , "F" ) #arreglo con forma ( 4 , 2 )

Para acceder a los elementos de un array unidimensional se usa exactamente el mismo método de los [] claudators de una lista.

Para el caso de los arrays multidimensionales se usan los corchetes con los índices juntos como en el caso de las matrices
a[ 5 , 7 ]
colocamos una cantidad de argumentos menor o igual a la dimensión tensorial del array.

Los índices negativos también pueden usarse de la misma manera que en las listas.

Se le llama slicing a cortar un array grande en arrays más pequeños, lo cual ya habíamos visto en las listas y que veremos en
el caso de los dataframes de pandas. Para ello se utilizan los mismos métodos de las listas de los corchetes y los dos puntos
[ inicio : final : paso ] en el caso de arrays unidimensionales
a[ 1 : 4 ]
a[ : 4 ]
a[ 1 : ]
a[ 0 : 3 : 2 ]
a[ : 4 : 2 ]
a[ 1 : : 2 ]
a[ : : 2]
Para arrays multidimensionales habrá que separar las consultas con comas, por ejemplo:
a[ 2 : , : 1 , -1 ]

Dado un dataframe, podemos generar otro dataframe de las mismas dimensiones con booleanos imponiéndoles una condición al
array como si se tratara de una variable, por ejemplo, dado un array a podemos obtener un array que tenga True cuando la
entrada es mayor que dos y false cuando no lo es haciendo:
arreglo_bool = arreglo > 4
print( arreglo_bool )
Este array booleano podemos introducirlo en las entradas de un array para generar el array filtrado. En este caso:
arreglo_filtrado = arreglo[ arreglo_bool > 2 ]
esto es similar a filtrar dataframes en pandas, lo cual veremos más adelante.

Por supuesto podemos hacer operaciones lógicas para filtrar un array:


arreglo_filtrado = arreglo[ ( arreglo_bool > 2 ) & ( arreglo_bool < 10 ) ]

De esta misma forma podemos modificar ciertos datos, por ejemplo:


arreglo[ arreglo > 4 ] = 10

Si el array es multidimensional, al filtrarlo puede que no se puedan completar las dimensiones para formar un array
correctamente, por lo que, cuando los filtremos, numpy nos devuelve un array aplanado con los entradas filtradas.

Los arrays son mutables, entonces las entradas de un array pueden modificarse simplemente invocándola y asignándole un valor
mediante el operador de asignación igual.
_________________________________________________________________________

*****1.2- Matrices y vectores con numpy*****

Los arrays numéricos de numpy pueden tratarse como vectores, Python define sus operaciones de la manera estándar, es decir,
dados dos vectores x,y (dos arrays de la misma longitud) para sumarlos escribimos simplemente:
x = np.array( [ 1 , 2 , 3 , 4 , 5 ] )
y = np.array( [ 9 , 5 , 3 , 6 , 4 ] )
print( x + y )

Para el producto escalar, simplemente multiplicamos un array por un escalar:


x = np.array( [ 1 , 2 , 3 , 4 , 5 ] )
y = np.array( [ 9 , 5 , 3 , 6 , 4 ] )
print( 3*x )

Dados dos arrays, podemos definir un nuevo aray que contenga la multiplicación de sus entradas respectivamente (no confundir
con el producto punto, cruz ni tensorial).
x = np.array( [ 1 , 2 , 3 , 4 , 5 ] )
y = np.array( [ 9 , 5 , 3 , 6 , 4 ] )
print( x * y )

Para elevear todos los elementos de un array a una potencia, les colocamos la potencia como si se tratara de un número:
x = np.array( [ 1 , 2 , 3 , 4 , 5 ] )
y = np.array( [ 9 , 5 , 3 , 6 , 4 ] )
print( x**2 )

Para realizar el producto punto usual se utiliza el método .dot( array ) sobre un array y se coloca el otro como argumento:
x = np.array( [ 1 , 2 , 3 , 4 , 5 ] )
y = np.array( [ 9 , 5 , 3 , 6 , 4 ] )
print( x.dot( y ) )
también poemos usar la función np.dot( primer , segundo ) por ejemplo:
x = np.array( [ 1 , 2 , 3 , 4 , 5 ] )
y = np.array( [ 9 , 5 , 3 , 6 , 4 ] )
print( np.dot( x , y ) )
una tercera forma es con el operador @, que nos permite directamente hacer el producto punto:
x = np.array( [ 1 , 2 , 3 , 4 , 5 ] )
y = np.array( [ 9 , 5 , 3 , 6 , 4 ] )
print( x@y )

Para crear una matríz vacía usamos la terminación .empty( tupla ) y una tupla (que veremos más adelante) que
indique las filas y las columnas. Por ejemplo
import numpy as np
a=np.empty( ( 2 , 3 ) )

Si queremos crear una matríz vacía pero con las mismas dimensiones de otra usamos la terminación
.empty_like(otramatriz) donde el argumento sea otra variable que contenga una matríz. Por ejemplo
b = np.empty_like( a )

Para crear una matríz nula, de puros ceros, usamos la terminación .zeros( ( dim1 , dim2 ) ) nuevamente con
una tupla en el argumento que indica las dimensiones de la matríz. Por ejemplo:
c = np.zeros( ( 2 , 3 ) )

Para crear una matríz de ceros con las mismas dimensiones de otra, podemos mezclar otra vez con, por ejemplo:
d = np.zeros_like( a )

Para una matríz de unos usamos las terminaciones .ones( tupla ) y .ones_like( matríz )

Para crear una matríz con los valores que uno quiera, podemos usar la función .matrix() en donde el argumento
es una lista de listas como las que vimos antes.

Para calcular las dimensiones de una matríz podemos usar la terminación .shape, va sin paréntesis (si argumentos) por ser
una propidedad y nos devolverá una tupla con las dimensiones.

Podemos imprimirlas con print y quedarán muy bonitas :)

Para acceder a las entradas de una matríz una vez importado numpy, simplemente hay que colocar corchetes y las entradas
separadas por comoa, como si fueran coordenadas a[ i , j ]. Las matrices de numpy son objetos mutables, por lo que podemos
alterar sus entradas simplemente igualando la netrada a[ i , j ] con el valor que queremos asignar. Para crear una matríz en
numpy desde cero, ususalmente se define una matríz vacía y se va rellenando con ciclos for mutando sus entradas. Por ejemplo,
el siguiente programa le pide las dimensiones al
usuario de una matríz y luego la pide las entradas para rellenarlas:
import numpy as np

print( "Este programa no permite crear una matríz con las dimensiones que usted elija." )
n = int( input( "Número de filas: " ) )
m = int(input( "Número de columpas: " ) )

a = np.empty( ( n , m ) )

for i in range( n ):
for j in range( m ):
a[ i , j ] = int( input( "Entrada {},{}: ".format( i , j ) ) )
print( "\n===Matríz A===\n" )
print( a )

Podemos calcular la traspuesta de una matríz con la propiedad .T, por ejemplo:
M = np.array( [ [ 1 , 2 , 3 ] ,
[4,5,6],
[7,8,9]])
print( M )
print( "\n" )
print( M.T )

Podemos comparar arrays para saber si son iguales, pero no de la manera usual, es decir, para saber si dos arrays son
iguales, digamos A y B, podríamos compararlos haciendo A == B, pero al hacer esto, numpy nos lanzará un error diciendo que
comparar A con B es ambiguo. Para saber si dos arrays son iguales usamos la función np.array_equal( array1 , array2 )
colocando como argumentos los arrays a comparar:
A = np.array( [ 1 , 2 , 3] )
B = np.arange( 1 , 4 )

print( np.array_equal( A , B ) )
_________________________________________________________________________

*****1.3- Tipos de datos, copias y vistas*****

Numpy tiene más tipos de datos que los que conocíamos:


i: integer
u: unsigned integer
f: float
c: complex float
b: boolean
m: timedelta
M: datetime
O: object
S: string
U: unicode string (codificación parecida al ASCII)
V: void (vacío)

Para saber el tipo de datos que contiene un array usamos la propiedad .dtype sobre el array. También se lo podemos poner
como
parámetro al constructor de los array, para determinar el tipo de dato al inicio.

Al usar el método .array(), existe un parámetro dtype que nos permite definir el tipo de dato que queremos que tengan los
elementos de dicho ndarray
a = np.array( [ 1 , 2 , 3 , 4 , 5 ] , dtype = "S" )
print( a )
a.dtype

Para el caso de los tipos de dato i, u, f, S y U, con el parámetro dtype también podemos definir el tamaño:
a = np.array( [ 1 , 2 , 3 , 4 , 5 ] , dtype = "c16" )
print( a )
a.dtype
a = np.array( [ 1 , 2 , 3 , 4 , 5 ] , dtype = "f16" )
Si queremos cambiar el tipo de dato de un array existente, usamos el método .astype()
print( "El array a es de tipo" , a.dtype )
b = a.astype( "i" )
print( "El array b es de tipo" , b.dtype )

Podemos colocar también tipos de datos de Python y no de numpy. Los arrays pueden contener un único tipo de datos, eso
sucede
por que numpy es justamente para optimizar la velocidad, al contener más tipos, como en las listas, el proceso se volvería
lento y los arrays serían objetos más complejos.

Para copiar un array y guardar la copia en otro usamos el método .copy() sin argumenmtos. Esto es común cuando manejamos
un
array cuyo valor no queremos que sea alterado, construimos una copia y trabajamos sobre ella.
a = np.array( [ 1 , 2 , 3 , 4 , 5 ] )
b = a.copy()

Una vista es una referencia que nos sirve para indicar que se cambie el array original. Es decir, es como una variable extra
que apunta a la misma dirección que el objeto original. Para crear estas referencias las guardamos en una variable usando
sobre el array original el método .view()
a = np.array( [ 1 , 2 , 3 , 4 , 5 ] )
b = a.view()

La propiedad .base se aplica a un array generado a partir de otro. Nos devuelve None si se trata de una copia o nos devuelve
el objeto original si se trata de una referencia (vista).

En ocasiones, al operar o crear array con ceros, numpy no nos devolverá un cero como tal, sino un número extremadamente
pequeño, esto se debe a errores en el cálculo numérico debido al hecho de que hay que convertir números desde base 2. Para
ocultar esta situación podemos modificar las opciones internas de numpy mediante el código:
np.set_printoptions( suppress = True )
esta función tiene muchos parámetros que cambian la configuración del numpy que se está ejecutando, es buena idea investigar
todos los parámetros en la documentación:
https://numpy.org/doc/stable/reference/generated/numpy.set_printoptions.html

_________________________________________________________________________

*****1.4- Bucles en arrays*****

Para el caso de los arrays, el for funciona exactamente igual en los arrays unidimensionales. Para el caso de los
bidimensionales podemos usar dos for. Es importante resaltar que cuando se trata de arrays con entradas de muchos tipos de
datos, al aplicarle un for numpy transforma todos los valores al tipo menos costoso de los que se encuentran en el arreglo,
por ejemplo, si hay enteros y strings, lo menos costoso son los strings y todos los datos serán transformados en strins. Lo
análogo sucede para los tridimensionales o n-dimensionales.

Hay una forma más sencilla, podemos hacer un reshape para aplanarlo y luego iterarlo en un for. Aunque el reshape es
computacionalmente más costoso, en el caso de millones de datos.

Hay un tipo especial de bucles que nos facilitan lo anterior, para ello usamos el método numpy.nditer(array), el cual
iterador implementado en numpy sobre el cual se puede iterar. Es lo mismo que con el reshape pero en este caso Python se
ahorra todo el costo computacional que mencionabamos.

El método .nditer() nos perimite cambiar el tipo de dato mientras iteramos, usando el parámetro op_dtypes = [string del tipo]
y el parámetro flags = [ "buffered" ]. El buffered nos hace un ahorro computacional de memoria, ya que si se necesita iterar
un array muy grande, este argumento permite iterar un elemento, destruirlo y pasar al siguiente. Podemos mezclar .nditer con
un array filtrado dentro, por ejemplo, un slicing o una condición.

Otro método es el numpy.ndenumerate( array ) que nos permite iterar con dos índices donde el primero guarda el índice
tensorial en forma de tupla de un elemento y el segundo guarda el elemento.
d = np.array( [ [ 1 , 2 ] , [ 3 , 4 ] , [ 5 , 6 ] ] )

for i , element in np.ndenumerate( d )


print( "El elemento con el índice tensorial {} es: {}".format( i , element ) )

_________________________________________________________________________

*****1.5- Concatenación de arrays*****

Para concatenar arrays podemos usar el método np.concatenate( ( array1 , array2 ) )

En el caso de dos arrays unidimensionales, el método solo nos juntará los elementos de los arrays en un array nuevo.

Para un array bidimensional agregamos el parámentro axis = con un valor 0 o 1. El valor 0 nos aplilará todos los
elementos en un solo array, por ejemplo, para un par da arrays de 3x2 nos quedará como resultado uin array de 6x2.
El parámetro 1 fusiona los elementos, por ejemplo, 2 arrays de 3x2 se convierten en un array de 3x4 ( Nota que se suman los
índices dependiendo del eje que elijamos ).
Se cumple lo análogo con arrays n-diemnsionales si tienen las dimensiones coincidentes correctas.

Otra forma de concatenar es a través del método numpy.stack( ( array1 , array2 ) , axis = ) donde axis solo puede tomar los
valores enteros mayores o iguales a cero. Este método genera un nuevo eje de tal forma que genera un array de dimensión
superior que junta un elemento de cierta dimensión con otro elemento de las misma dimensión correspondiente al otro
array. Por ejemplo, dos arrays de dimensión 1 con tres elementos, pueden juntarse de dos formas, un array de dimensión dos
que reparte sus 6 elementos totales, en ( 2 , 3 ) o ( 3 , 2)

Podemos hacer esto de manera más distinguida con los métodos .vstack(), .hstack() y dstack(), es decir, estos métodos hacen
lo mismo que el concatenate() con distintos ejes, no los apila como el np.stack().

_________________________________________________________________________

*****1.6- Dividiendo arrays*****

Podemos dividir un array unidimensional de n elementos en un array con n subarrays gracias a la función
np.array_split( array , k ) donde k es el número de subarrays que queremos generar.

Supongamos que tenemos un array n dimensional y lo queremos subdividir en q subarrays, recordemos del algoritmo de la
división que existen m,r tales que:
n = mq + r
nota que este número podemos escribirlo como:
n = ( q + 1 )r + q( m - r )
entonces los elementos del subarray quedan divididos en r arrays con q + 1 elementos y m - r arrays de q elementos.

Otro método para dividir arrays es el método np.split(), este funciona igual que array_split() y tiene los mismos parámetros,
pero solo funciona si la división que necesitamos es entera. En caso de que no lo sea, lanza un error.

Para el caso multidimensional podemos seleccionar el eje por el cual queremos dividir, para ello escribimos el parámetro axis
como lo hicimos antes, donde el axis es la profundidad del tensor a la cual partiremos.

Así como existen .vstack(), .hstack() y dstack(), también existen .vsplit(), .hsplit() y dsplit()
_________________________________________________________________________

*****1.7- Búsquedas y ordenamientos en un array*****

Para buscar elementos en un array independientemente de la dimensión usamos el método numpy.where(condición de array).
Este
método nos devuelve una tupla con los índices que cumplen la condición en el caso unidimensional. Por ejemplo:
x = np.array( [ 1 , 0 , -1 , 0 , 2 , 0 , -2 , 0 ] )
i = np.where( x == 0 )
print( i )

Para el caso multidimensional se hace del mismo modo, pero el método nos devuelve una tupla de arrays que contienen los
índices tensoriales de los elementos que cumplen la condición, hay que tomar un número de cada array para localizar los
índices correspondientes, es decir, los índices vienen separados.

Dado un array numérico unidimensional ordenado de manera creciente, el método np.searchsorted( array , valor ) nos devuelve
el primer número de índice necasario para que, al agregar el valor al array, este conserve su orden. Si el array no está
ordenado, el método primero lo ordena sin modificar el original, y luego devuelve el índice. Podemos agregar un parámetro
side = "left" o "right". El parámetro "right" nos devuelve el último indice necesario para que al agregar el valor al array,
este conserve su orden (Esto es más notorio cuando hay datos repetidos de un mismo tipo).

En lugar de introducir un valor, podemos introducir una lista y np.searchsorted( array , lista ) nos devuelve un array que
nos muestra los índices necesarios para hacer lo propio pero por separado.

El método np.sort( array ) ordena los valores de un array unidimensional, ya sea en forma creciente si es numérico, o
siguiendo el orden lexicográfico si son strings. En caso de ser valores booleanos, primero coloca los False y luego los
True. Si aplicamos np.sort() a un array multidimensional, acomoda en la dimensión más profunda. El resultado de np.sort() nos
da una copia ordenada, es decir, el array original no se modifica, pero la copia la podemos guardar en una variable nueva.

Para el caso multidimensional np.searchsorted( array , valor ) ordena los subarrays. Aunque podemos ordenar por dimensiones
agragando el parámetro axis = , como hicimos antes, por defecto el índice está colocado en la dimensioón más profunda.

_________________________________________________________________________

*****1.8- Aleatoriedad en numpy*****

Numpy tiene un submódulo llamado random, podemos importarlo a partir de un from si no queremos molestarnos tanto
escribiendo
np.random.loquesea:
from numpy import random
Hay que tener cuidado, Python ya tiene por sí mismo un módulo random, que solo se importa con un import, no es el mismo
módulo que el de numpy pero tiene casi las mismas funciones

Podemos generar número aleatorios. Una forma sencilla es usar la terminación .randint( inicio , final , cuántos ) para
generarlos. Si solo colocamos un argumentos se entiende el maximo del intervalo. El tercer argumento controla cuántos
números aleatorios queremos, devolviéndonos un array, aunque podemos omitirlo, e incluso solo pasarle un argumento. Por
ejemplo:
from numpy import random
random.randint( 5 , 25 , size = 9 )
genera un array con 9 números aleatorios entre 5 y 24. Si queremos un tensor de números aleatorios, en el parámetro size
colocamos una tupla con las dimensiones.
random.randint( 5 , 25 , size = ( 5 , 4 ) )
Para generar un número en el intervalo [ 0 , 1 ] real, usamos nprandom.rand(), si queremos un array con esta clase de números,
colocamos las dimensiones del tensor en su argumento.
np.random.rand( 2 , 5 )

Podemos extraer una entrada aleatoria de un arreglo unidimensional con el método np.random.choice( array ), este método solo
acepta arrays unidemensionales. Por supuesto podemos aplanar un array para tomar todos los valores de un tensor.

Podemos generar un tensor aleatorio con números de un array introduciendo el parámetro size = (tupla). Por ejemplo:
l = np.array( [ -1 , 1 , 3 , 5 , 9 , 10 ] )
x = np.random.choice( l , size = ( 2 , 3 ) )
ahora x es un array de dimensiones ( 2 , 3 ) de números seleccionados al azar de l.

Podemos introducir el parámetro p = lista , con una lista de números entre 0 y 1 que sumen 1 y que sea del mismo tamaño que
el array donde tomamos los valores, para modificar las probabilidades de la lista, ya no serán equiprobables. Por ejemplo:
arreglo = np.array( [ -1 , 1 , 3 , 5 , 9 , 10 ] )
x = np.random.choice( arreglo , p = [ 0.1 , 0.3 , 0.1 , 0.05 , 0.45 ] , size = ( 2 , 3 ) )

En Python podemos permutar arrays mediante dos métodos np.random.shuffle() y el np.random.permutation(). El primero se le
aplica a un array y este queda modificado, se trata de una mezcla aleatoria del array original. El segundo permuta una copia
de un array la cual hay que guardar en una variable nueva, pues el original no se modifica.

Por supuesto podemos modificar la semilla con .seed() y colocando un entero dentro:
np.random.seed( 3 )

Podemos calcular un array de números de acuerdo a una distribución uniforme( es decir, todos los valores equiprobables) entre
a y b mediante el comando np.random.uniform( a , b , n ), donde n es el número de muestras de la distribución:
x = np.random.uniform( a , b , n )
print( x )
si ahora queremos que se distribyan como una normal, usamos np.random.randn() pero aparecerán como una distribución
estandarizada (z-sccore), es decir con una media en 0 y una desviación estándar de 1. Si queremos modificarla, podemos hacer
el siguiente truco, si sigma = 2.5 y mu = 5.5, el array con media en 5.5 y desviación estándar de 2.5 será:
x = np.random.randn(100)
y = mu + sigma*x

_________________________________________________________________________

*****1.9- Funciones univesales*****

Una función universal es una función que podemos definir y que puede actuar sobre los n-arrays, es como si definieramos
una función dentro de la clase array() de la paquetería Numpy, como lo vimos en POO.

Podemos saber si una función es una función universal de numpy usando la función type() sobre la función, por ejemplo
type( np.multiply )
nos saldrá <class 'numpy.ufunc'>

Para crear una función universal nuestra, definimos una función, más adelante le asignamos el método
np.frompyfunc( función , entradas, salidas), por ejemplo:
def my_sum( x , y ):
retunr x + y

my_sum = np.frompyfunc( my_sum , 2 , 1)


a = np.array( [ 1 , 0 , 7 ] )
b = np.array( [ -1 , 2 , 5 ] )

print( my_sum( a , b ) )
esta función my_sum() ahora suma dos arrays de cualquier dimensión (pero la misma) y nos devuelve un array con la suma
vectorial de los primeros arrays, nota que no se tiene que llamar con np, es decir, no es np.my_sum().

El ejemplo anterior solo era un ejemplo de como crear una función universal, pero numpy ya tiene sus propias funciones.

El método np.add(,) suma dos arrays de la misma dimensión, lo análogo hace np.susbtract().

El método np.multiply(,) multiplica arrays elemento a elemento (no es ningún producto matricial ni nada de eso), lo análogo
hace el método np.divide(,).

El método np.power(,) eleva el elemento del primer array al elemento del segundo array respectivamente (es decir, deben de
tener las mismas dimensiones).

El método np.mod() y el np.reminder() calculan el módulo del primer elemento con el segundo elemento respectivamente.

El método np.divmod() nos da una tupla de dos arrays, el primero tiene los cocientes y el segundo tiene los restos de la
división euclidiana.

El método np.absolute(array) devuelve un array con los valores absolutos del array argumento.

Todas estos métodos son operaciones vectiorizadas, es decir, actúan elemento a elemento de arrays.

_________________________________________________________________________

*****1.10- Redondeo y matemáticas en numpy*****

Hay 5 formas de redondear en numpy:


np.trunc() - para truncar, solo deja la parte entera
np.fix() - para truncar, solo deja la parte entera, es lo mismo que np.trunc()
np.around() - para redondear (puede ponerse un segundo argumento, los decimales que se requieren)
np.floor() - para redondear a la baja
np.ceil() - para redondear a la alza

Para sumar todos los elementos de un array aplicamos simplemente la función np.sum(), donde esta suma no depende de la
dimensión del array.

Podemos aplicar nuevamente el parámetro axis a la función np.sum(), lo cual nos devolverá un array con la suma
correspondiente al eje seleccionado.

La función np.cumsum() nos sirve para calcular suma acumulada y funciona exactamente igual que la función np.sum() como
lo vimos arriba.

Si en lugar de sumar queremos restar usamos la función np.diff() para calcular las diferencias sucesivas de derecha a
izquierda. Este método funciona exactamente igual que np.sum() y np.cumsum() pero podemos agregarle un parámetro
adicional,
el parámetro n = el cual nos permite calcular más de una vez las diferencias sucesivas.

Para multiplicar todos los elementos de un array aplicamos simplemente la función np.prod(), donde esta suma no depende de
la dimensión del array. A esto podemos agregarle nuevamente el parámetro axis = como lo hicimos nateriormente con las sumas
y diferencias.

Con la función np.cumprod() calculamos el producto acumulado del array. Pero podemos agregar nuevamente el parámetro axis
=
para hacer le producto acumulado en las distinatas dimensiones como lo hicimos en la función np.cumsum()

Dado un array, podemos calcular el valor máximo del array aplicándole el método .max(), mientras que el mínimo se encuentra
con .min(). Podemos encontrar los índices del máximo y mínimo con .argmax() y .argmin(). Podemos encontrar la diferencia
entre el mínimo y el máximo con la función .ptp()

Podemos calcular los percentiles con la función np.percentile( array , percentil ), mientras que la mediana se calcula
mediante np.median( array ). Si se trata de un tensor o matríz, podemos agregar el eje axis = para especificar el eje con
el que se calcularán. Podemos calcular la desvación estándar y la varianza mediante np.std() y np.var(). El promedio lo
calculamos con .mean()

Se puede aplicar únicamente el logaritmo natural a un array de numpy con la función np.log(array), pero podemos construir
el logaritmo en cualquier otra base importándolo de la librería math y volviéndola una función universal de la siguiente
forma:
from math import log

nplog = np.frompyfunc( log , 2 , 1 )


print( nplog( np.array( [ 100 , 1000 , 50 , 500 ] ) , 10 ) )

Para calcular el MCM usamos la función np.lcm.reduce( array ), mientras que para el MCD se usa np.gcd.reduce(array)

En numpy el número π se obtiene con np.pi, mientras que infinito es np.infty

En numpy disponemos de los métodos:


np.sin()
np.cos()
np.tan()
para calcular el seno, coseno y tangente de los elementos de un array (los valores se consideran en radianes)

Para calcular conversión de ángulos a radianes y de radianes a ángulos respectivamente:


np.deg2rad()
np.rad2deg()

Para calcular el arcoseno, arcocoseno y arcotangente y hallar ángulos (el resultado es devuelto en radianes)
np.arcsin()
np.arccos()
np.arctan()

En numpy disponemos de los métodos:


np.sinh()
np.cosh()
np.tanh()
para calcular el seno, coseno y tangente hiperbólicos de los elementos de un array (los valores se consideran en radianes)

Para calcular el arcoseno, arcocoseno y arcotangente hiperbólicos y hallar ángulos (el resultado es devuelto en radianes)
np.arcsinh()
np.arccosh()
np.arctanh()
todas estas operaciones también pueden aplicarse a arrays de numpy.
Para calcular la desviación estándar de las entradas se usa np.std( array )

En el caso de usar matrices y vectors, numpy ya tiene implementados ciertos métodos para realizar cálculos del mundo del
álgebra lineal. Estas implementaciones se encuentran en el submódulo .linalg que obtenemos al importar numpy
automáticamente.
Otra forma es importar el módulo aparte para quitar el np:
from numpy import linalg

Podemos usar muchas funciones de esta librería para no implementar nada desde cero, por ejemplo, para la norma usual:
linalg.norm( x )
para el producto matricial se toman dos matrices y se multiplican d ela siguiente forma:
np.matmul( m1 , m2 )
y mediante el operador @ mediante m1 @ m2
más información de este submódulo podemos encontrarla en:
https://numpy.org/doc/stable/reference/routines.linalg.html
Más adelante en la sección de álebra lineal se encuentran las herramientas más usadas de esta librería.

_________________________________________________________________________

*****1.11- Conjuntos en Numpy*****

Se pueden generar conjuntos con Numpy, para ello generamos un array y le aplicamos la función np.unique(array). Con esta
misma función obtenemos una lista de los datos únicos de un array al agregar el argumento returns_counts = True:
arreglo = np.unique( x , return_counts = True )

Para unir dos conjuntos usamos la función np.union1d(,), para la intersección aplicamos la función np.intersect1d(,),
para la diferencia np.setdiff1d(,), para la diferencia simétrica np.setxor1d(,)
El 1d es por 1-dimensional.

_________________________________________________________________________________________________________
____________________

Capítulo 2: Módulo pandas y dataframes

*****2.1- Dataframes y su construcción*****

Un dataframe es una estructura bidimensional mutable de datos con los ejes etiquetados, lo que comúnmente conocoemos
como
una "tabla". Cada fila representa un dato diferente y cada columna representa una variable diferente.

Para crear dataframes en Python necesitaremos importar la paquetería "pandas", es común colocarle el alias "pd". La librería
pandas es una librería basada en numpy, por lo que para manejarla, hay que tener idea de numpy, además es una ventaja, pues
hereda toda la velocidad de numpy.

Para definir un dataframe, nos basamos en estructuras de datos como diccionarios, listas, listas de diccionarios etc.

Podemos crear un dataframe a partir de un diccionario. Es común cuando tenemos acceso a las columnas por separado. Primero
creamos un diccionario con la siguiente sintaxis:
nombre = { "clave1":lista1 , "clave2":lista2 }
luego se lo pasamos a pandas para crear el dataframe df con la sintaxis:
df = pandas.DataFrame( data = nombre )
por ejemplo:
datos = { "x":[ 1 , 2 , 3 , 4 , 5 ] , "y":[ 2 , 4 , 6 , 8 , 10 ] }
df = pandas.DataFrame( data = datos )
nota que aparecerá la tabla pero las filas tendran un nombre predeterminado que enumera comenzando por 0

Para construir un dataframe con una lista de listas, primero definimos una lista de listas, en donde cada sublista
indica la fila, esto lo hacemos con la sintaxis:
datos = [ [ 1 , 2 ] , [ 2 , 4 ] , [ 3 , 6 ] , [ 4 , 8 ] , [ 5 , 10 ] ]
luego se lo pasamos a pandas de la misma forma.

El nombre de las columnas podemos indicarlo con un segundo argumento, que será una lista con los nombres de las columnas:
df = pandas.DataFrame( data = datos , columns = [ "x" , "y" ] )
si no indicamos el parámetro columns, Python numera las columnas iniciando con 0.

De igual manera podemos modificar el nombre de las filas (índices) especificando el argumento index = e igualando
a una lista con los nombres de las filas
df = pandas.DataFrame( datos , columns = [ "x" , "y" ] ,
index = [ "Primero" , "Segundo" , "Tercero" , "Cuarto" , "Quinto" ] )

No conviene tanto el parámetro columns cuando la data viene de un diccionario, el parámetro columns no cambia los nombres,
sino que muestra las columnas que ya han sido creadas desde el diccionario, si colocamos un nombre nuevo intentando cambiar
uno viejo, esto omite la columna que queríamos y nos mostrará una columna con valores NaN, pero si puede servir para omitir
las columnas que no aparecen en la claves. Es decir, crear un subdataframe.

Podemos crear un dataframe pero con una lista de diccionarios. Vamos a crear el mismo dataframe que en clase anteriores:
datos = [ { "x":1 , "y":2 } ,
{ "x":2 , "y":4 } ,
{ "x":3 , "y":6 } ,
{ "x":4 , "y":8 } ,
{ "x":5 , "y":10 } ]
df = pandas.DataFrame( data = datos )

Recordemos que la función zip() unía dos listas en una lista de tuplas, de esta forma también podemos crear un dataframe
de la siguiente forma:
x=[1,2,3,4,5]
y = [ 2 , 4 , 6 , 8 , 10 ]
datos = zip( x , y )
df = pandas.DataFrame( data = datos , columns = [ "x" , "y" ] )
es necesario colocar el nombre de las columnas o se colocaran por default numerándolos a partir de 0.

Si vamos a crear un dataframe con un diccionario, recordemos que las claves son el nombre de las columnas. Podemos hacer
que las claves sean los nombres de las filas. Para ello necesitamos agregar la terminación .from_dict() y el parámetro
orient="index". Luego podemos modificar el nombre de las columnas con el parámetro columns. Por ejemplo:
datos = { "fila1":[ 1 , 4 , 7 ],
"fila2":[ 2 , 5 , 8 ] ,
"fila3":[ 3 , 6 , 9 ] }
df = pandas.DataFrame.from_dict( data = datos , orient = "index" , columns = [ "A" , "B" , "C" ] )

Es usual mezclar los dataframes hechos de listas con las funciones que vimos en la parte de funciones lambda, en particular
con la función map(). El siguiente código forma un dataframe con una lista de palabras, sus longitudes, su primera letra,
su última letra y nos dice si es palíndromo:
import pandas as pd

words = [ "sol" , "ala" , "cama" , "duro" , "bueno" , "kayak" , "marea" , "rotor" , "misterio" , "acurruca" ]
dicc = { "Palabra": words ,
"Longitud": map( len , words ) ,
"Primera": map( lambda x : x[ 0 ], words ) ,
"Ultima": map( lambda x : x[ -1 ], words ) ,
"Palíndromo" : map( isPalindrome , words ) }

tabla = np.DataFrame( data = dicc )


print( tabla )
donde isPalindrome() es una función previamente preparada.

Si queremos transformar una de las columnas en los índices (filas enumeradas) usamos la termincaión .set_index(), donde el
parámetro es la clave de la columna que queremos colocar. En nuestro caso sería
tabla = tabla.set_index( "Palabra" )
pero nos queda la clave estorbando aún. Para quitarla usamos el código .index.names = [None] . En nuestro caso:
tabla.index.names = [None]

Para visualizar de manera elegante un dataframe, uno de los métodos es colocar la terminación .head() donde el argumento es
el número de filas que queremos observar sin contar los nombres de las columnas.
_________________________________________________________________________

*****2.2- Dimensiones de los DataFrames*****

Con la terminación .shape sin argumento aplicada a un dataframe, conseguimos una tupla con las dimensiones del dataframe
sin contar etiquetas. Por ejemplo.
df.shape
la primera columna es el número de filas y la segunda el número de columnas.

Para acceder al número de filas y de columnas respectivamente, obviamente escribimos df.shape[0] y df.shape[1]. Aunque
también podemos calcular el número de filas aplicándole la función len() al dataframe:
len( df )

Podemos calcular el número de elementos totales en un dataframe con la propiedad .size sin argumentos que por supuesto es
equivalente a multiplicar el número de filas por el de columnas con df.shape[0]*df.shape[1]

Para calcular el número de dimensiones (el número de índices de un tensor) usamos la propiedad .ndim sin argumentos.
En el caso de un datafrem usual será 2.
_________________________________________________________________________

*****2.3- Extracción de columnas*****

Para extraer columnas de un Dataframe hay varias formas.

La primera es bastante sencilla, simplemente colocamos el nombre del dataframe y el nombre de la columna entre corchetes,
recordemos que el nombre es por lo general un string clave ya sea de un diccionario o de una lista. En el caso del dataframe
de la clase pasada, pero con el nombre df, usamos la sintaxis:
df[ "Primera" ]
para mostrar la columna que tiene por nombre "Primera".

La propiedad .columns aplicada a un dataframe nos devuelve una lista con el nombre de las columnas del dataframe, con esto
existe una segunda forma de extraer una columna seleccionando el nombre con .columns[n] (¡¡¡ojo!!! que es entre corchetes y
no entre paréntesis), para obtener el nombre de la columna, por ejemplo, el código:
print( df.columns[ 0 ] )
imprime el string "Primera" que corresponde a la columna 0. Para seleccionar entonces la columna podemos escribir:
print( df[ df.columns[ 0 ] ] )
mezclando así el primer método con .columns[]

Una forma más es usar el método .loc[ : , "clave" ], donde el primer argumento ":" indica que nos devolverá todas las filas
y el segundo argumento es el nombre de la columna. Por ejemplo:
print( df.loc[ : , "Primera" ] )

Se puede hacer lo mismo con la terminación .iloc[ : , n ] pero en este caso el segundo argumento es la posición de la
columna y no su clave. Por ejemplo:
print( df.iloc[ : , 0 ] )

En todos los métodos podemos seleccionar más de una columna. En algunos de ellos hay que sustituir el valor o clave por
una lista que contenga los valores o claves que queremos extraer. Colocamos los cuatro métodos que hacen los mismo,
para imprimir las columnas "Primera" y "Segunda" que en esta ocasión suponemos que son columnas contiguas de un dataframe
df:
print( df[ [ "Primera" , "Segunda" ] ] )

print( df[ df.columns[ [ 0 , 1 ] ] ] )

print( df.loc[ : , [ "Primera" , "Segunda" ] ] )

print(df.iloc[ : , [ 0 , 1 ] ] )

Si nosostros vemos el tipo de datos que es una columna extraida:


print( type( df[ "Primera" ] ) )
Python nos dirá que es un tipo "series" de pandas que es una especie de vector columna a la cual podemos aplicarle algunos
métodos de dataframes y de listas. Si no queremos una serie de pandas podemos extraer un dataframe con una sola columna
con
un "truco mañoso", ponemos una lista con un solo elemento dentro del slicing del dataframe:
columna = df[ [ "Primera" ] ]
los objetos series los revisamos más adelante.

Podemos hacer una variante usando la sintaxis de los dos puntos para imprimir desde una columna hasta otra. Colocamos
nuevamente los cuatro ejemplos que hacen lo mismo que lo de arriba, a excepción del primero.
print( df[ df.columns[ 0 : 2 ] ] )

print( df.loc[ : , "Primera" : "Tercera" ] )

print( df.iloc[ : , 0 : 2 ] )

Al aplicar la función list( dataframe ) a un dataframe, obtenemos una lista con los nombres de las columnas como lo
hicimos con la propiedad .columns.
_________________________________________________________________________

*****2.4- Extracción de filas*****

Para seleccionar filas, los métodos más flexibles y usados son el .loc[] y el .iloc[]. Por default, estos dos métodos
toman como primer argumento las filas de un dataframe y el segundo como las columnas, por lo que si solo colocamos
un argumento, estaremos manejando solo las filas. Los métodos usados son exactamente igual que en las columnas pero
ahora en la poscición de las columnas. Para una sola fila podemos usar la siguiente sintaxis:
print( df.loc[ "fila 1" ] )

print( df.iloc[ 0 ] )
para usar .loc ya deberían haberse cambiado el nombre de las filas y no dejar los enteros por defecto, como lo hicimos antes
df = pd.DataFrame( lista )
df = df.set_index( "filas" )
df.index.names = [None]
si no las hemos cambiado, se usan los números pero en string.

Para imprimir más de una fila, usamos listas:


print( df.loc[ [ "fila 1" , "fila 2" ] ] )

print( df.loc[ [ 0 , 1 ] ] )

Para imprimir varias filas contiguas usamos los dos puntos ":", es decir:
print( df.loc[ "Primera" : "Segunda" ] )

print( df.iloc[ 0 : 2 ] )

Para extraer entradas de un dataframe o subdata frame, podemos mezclar ambas sisntaxis, para las columnas
y para las filas.

Se pueden modificar las entradas de un dataframe simplemente invocando la entrada y con el operador = asignando un valor,
justo como los diccionarios.

Para crear una columna nueva en un dataframe, podemos simplemente definir el nombre de la columna como si se tratara de
crear una nueva clave-valor de un diccionario. Si lo igualamos a un solo valor, todas las filas de esa columna tomarán el
mismo valor, por ejemplo:
df[ "Nueva" ] = 0
si igualamos a una lista con la misma cantidad de valores que las filas del dataframe, obtendemos una nueva columna con los
valores ordenados de arriba a abajo:
df[ "Nueva" ] = [ 11 , 12 , 13 , 14 , 15 ]
podemos crear columnas basándonos en columnas ya existentes y haciéndoles operaciones:
df[ "Nueva" ] = 3*df[ "x" ] + 2
otra forma de hacer esto es con funciones y con el método .apply( func ) aplicado a la columna, por ejemplo:
def f( x ):
return 3*x + 2

df[ "Nueva" ] = df[ "x" ].apply( f )


o con funciones lambda:
df[ "Nueva" ] = df[ "x" ].apply( lambda x : 3*x + 2 )
ya que las series de pandas son tratadas como arrays de numpy.
Podemos jugar con un parámetro index = en .apply(), que nos aplicará funciones a filas y columnas.

También podemos aplicar funciones que tengan más de un argumento, especificando los argumentos faltantes después de la
función:
def f( x , y = 1 ):
return 3*x + 2*y
df[ "Nueva" ] = df[ "x" ].apply( f , y = 7 )

_________________________________________________________________________

*****2.5- Métodos de DataFrames*****

El método .head( n ) sirve para visualizar las primeras n filas del dataframe. Si se deja sin parámetro Python muestra 5
por defecto.
El método .tail( n ) sirve para visualizar las últimas n filas del dataframe. Si se deja sin parámetro Python muestra 5 por
defecto.

El método .sample( n ) sirve para visualizar n filas aleatorias del dataframe. Si se deja sin parámetro Python muestra 5 por
defecto.

El método .sort_values( "columna" ) nos ordena el dataframe de manera descendente con respecto a una columna que le
pasemos
por parámetro.
df.sort_values( "Longitud" )
Si queremos que sea ascendente colocamos el parámetro ascending = False:
df.sort_values( "Longitud" , ascending = False )
si queremos que sea permanente, lo guardamos o le colocamos el parámetro inplace = True
df.sort_values( "Longitud" , ascending = False , inplace = True )

El método .copy() sin argumentos nos sirve para realizar una copia de un dataframe y guardarla en una variable. Esto es útil
porque el dataframe se pasa por referencia a funciones, y modificará al dataframe original.

Para ver un dataframe simplemente colocamos el nombre de la variable que lo guarda.

Para cambiar el nombre de las columnas de un Dataframe usamos el método .rename( columns = dic , inplace = True ) con los
parámetros que se muestran. El parámetro columns debe tener asignado un diccionario cuyas claves sean los antiguos nombres
de las columnas a cambiar y como valor contengan los nuevos nombres, por ejemplo:
df.rename( columns = { "Primera":"Nueva1" ,
"Segunda":"Nueva2" } ,
inplace = True )
es importante el parámetro inplace, pues si no lo colocamos, los cambios no son guardados, aunque podemos guardar el
dataframe sin usar inplace = sobreescribiéndolo en sí mismo.

Se puede cambiar el nombre de las filas con el mismo método pero utilizando el parámetro index en lugar del columns, por
ejemplo:
df.rename( index = { "fila 1":"Nuevafila1" ,
"fila 2":"Nuevafila2" } ,
inplace = True )

Para obtener una lista con el nombre de las columnas podemos usar el método .columns sin argumento. Podemos cambiar el
nombre de las columnas asignando una lista con los nuevos nombres usando el método unpacking. Por ejemplo:
df.columns = [ "Nuevaprimera" , "Nuevasegunda" ]

Podemos insertar una columna con el método .insert( loc = índice_columna , column = nombre_columna , value = lista ), por
ejemplo:
df.insert( loc = 1 , column = "Nueva" , value = [ 1 , 2 , 3 , 4, 5 ] )
si la columna que seleccionamos ya tiene datos, entonces las desplazará a la derecha. Si en el parámetro value no se le pasa
una lista sino un valor, entonces le asigna esa valor a todas las filas de la columna.

Podemos agregar observaciones agregándolas como en las listas con el método .append().

Podemos borrar una fila o columna con el método .drop(), debemos usar ciertos parámetros con la sintaxis
.drop( labels = lista , axis = número 0 o 1), donde labels contiene una lista con el nombre de las columnas o filas a borrar
y el parámetro axis debe ser 1 para columnas o 0 para filas. Por ejemplo:
df = df.drop( labels = [ "Primera" , "Segunda" ] , axis = 1 )
borra dos columnas de nuestro dataframe. Este método no guarda los cambios, por lo que hay que agregar el parámetro
inplace=True como vimos antes para alterar el dataframe original o asignar el dataframe con el método a una variable como se
hizo arriba

Otra forma de eliminar columnas es con el método .pop( nombre_columna ), donde el argumento es el nombre de la columna
que se
quiere remover, este método sí altera directamente el dataframe y por defecto devuelve la columna eliminada, por lo
que si se quiere guardar la columna eliminada, puede asignarse a una nueva variable. Por ejemplo:
columna_eliminada = df.pop( "Primera" )

Si queremos agregar nuevamente la columna eliminada, podemos definir la nueva columna simplemente definiendo una entrada
como se hace con los diccionarios, es decir:
df[ "Primera" ] = columna_eliminada
la columna aparecerá al final.

Podemos agregar una columna que tenga etiquetas que ordenen un parámetro (hace un ranking). Para ello llamamos una
columna
y le agregamos el método .rank(), por ejemplo, si queremos poner una etiqueta que ordene de acuerdo a la columna "Primera"
hacemos:
df.[ "Primera" ].rank()
Si queremos etiquetar de forma descendente la agregamos el parámetro ascending = False, es decir:
df.[ "Primera" ].rank( ascendig = False )

El método .nunique() nos dice cuántos datos distintos hay en un dataframe para cada columna. Por ejemplo
df.nunique()

si solo queremos ver los datos distintos de una columna usamos el método .unique(), pero hay que llamarla, en este caso:
df[ "Primera" ].unique()
en este caso se nos devuelve una lista con los valores distintos, los valores no están ordenados necesariamente. Si ahora
queremos contarlos, aplicamos la función .value_counts() a la columna, nos devuelve una serie que tiene por índices los
datos distintos y por valores el número de apariciones (frecuencia absoluta):
df[ "Primera" ].value_counts()
si requerimos el porcentaje (frecuencia relativa) le pasamos el parámetro normalize = True:
df[ "Primera" ].value_counts( normalize = True )

Podemos filtrar la filas con valores repetidos respecto a una columna con el método .duplicated(). Este se aplica a una
columna de un dataframe que se ha llamado. Al método se le aplica el parámetro keep = . Hay tres opciones: False, se queda
con las columnas que tienen valores repetidos y eliminan los que tienen valores únicos. Por ejemplo:
dfrepetidos = df["Primera"].duplicated( keep = False )
print( dfrepetidos )

Si agregamos "first" o "last" (como strings) al parámetro keep, python conserva los últimos o los primeros valores repetidos
respectivamente.

El método .drop_duplicates() aplicado a un dataframe eliminará los datos repetidos basados en una columna del dataframe.
Necesita de dos parámetros, el keep = que anteriormente se explica y el parámetro subset = "nombre_columna" a la cual
se le da el nombre de la columna en la que se va a usar, esta función nos devuelve un nuevo dataframe, pero hay que
guardarlo en una nueva variable o colocar el parámetro inplace = True :
df_without_duplicated = df.drop_duplicates( keep = first , subset = "Primera")

Podemos quedarnos con n filas con el menor valor con respecto a una columna con el método .nsmallest( n, nombre_columna )
en donde el primer parámetro es el número de filas que queremos y el segundo es el nombre de la columna a considerar.
Lo análogo sucede con los valores más grandes gracias al método .nlargest()

Para calcular el tipo de dato de cada columna, aplicamos la propiedad .dtypes sin argumento alguno. Al igual que en los
arrays, podemos convertir los datos a uno adecuado en los dataframes, pero en este caso usamos la función .convert_dtypes():
df.convert_dtypes()

El método .describe() aplicado a un dataframe, nos devuelve otro dataframe con un resumen que contiene datos estadísticos
básicos, como el número de registros, el promedio, la desviación estándar, el mínimo, etc. Este método solo hace el resumen
para variables de tipo numérico, si queremos un resumen de variables que no son numéricas, le agregamos el parámetro
include = "object":
df.describe( include = "object" )
o podemos incluir todo colocando "all" como argumento de include():
df.describe( include = "all" )
Podmeos obtener información computacionalmente más ténica al aplicarle al dataframe el método .info():
df.info()

Podemos detallar la memoria que ocupa un dataframe mediante el método .memory_usage() con el parámetro deep = True, por
ejemplo:
df.memory_usage( deep = True )

En ocasiones los datos en los dataframes suelen venir en forma de conjuntos de datos en Python, por ejemplo, pueden ser
listas o tuplas, sin embargo púede que no sea posible extraerlos directamente porque pandas los guarda como string. Para
convertirlos a un tipo utilizable en Python necesitamos importar el módulo ast y aplicar la función . literal_eval( str )
al string:
import ast

lista_string = "[ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ]"


lista = ast.literal_eval( lista_string )
print( type( lista ) )

Si una de las columnas contiene datos agrupados como filas o tuplas, es posible desacoplar estos datos y colocar varios
registros en su lugar con cada dato utilizando sobre una fila la función .explode():
df[ "Primera" ].explode()

La propiedad .values aplicada a una columna nos devuelve un array con los valores de la columna:
df[ "Primera" ].values
si se la aplicamos a un subdataframe, nos devuelve un array donde cada entrada es una lista con los valores de cada fila,
por ejemplo:
filas = df[ [ "Primera" , "Segunda" ] ].values

for fila in filas:


print( '"Primera": {}, "Segunda": {}'.format( fila[0] , fila[1] ) )

En muchas ocasiones los dataframes tendrán valores en notación científica. Para quitarla escribimos la linea:
pd.options.display.float_format = "{:.1f}".format
el 1f controla el número de decimales que queremos que contengan nuestros datos.

Podemos aplicar las funciones matemáticas de numpy a columnas del dataframe dándoselas como métodos:
df[ "Kilogramos" ].sum()
O para calcular el q-ésimo cuantíñ}l:
df[ "Kilogramos" ].quantile( q = 35 )
_________________________________________________________________________
*****2.6- Bucles en dataframes*****

Podemos iterar sobre filas de dataframes tal como lo haciamos con listas o tuplas. Para ello necesitaremos de dos métodos,
el método .iterrows() y el .itertuples() combinados por supuesto con un for.

el método .iterrows() nos permite tomar dos índices en el for, el primero de ellos nos indica la posición de la fila
(incluso si ya cambiamos sus nombres)y el segundo guarda el contenido de la fila como un vector, como cuando imprimimos una
fila de un dataframe. Por ejemplo
for i, contenido in df.iterrows():
print( "Esta es la fila {} y contiene lo siguiente: \n{}".format( i , contenido ) )

El método .itertuples() nos devuelve la información de cada fila pero en tuplas, pero solo nos da un índice para iterar, por
ejemplo:
for i in df.itertuples():
print( i , end = \n\n )

Para iterar las columnas, podemos crear una lista con el nombre de las columnas haciendo list( df ), por ejemplo:
columns = list( df )

for i in columns:
print( "Columna {}:\n{}".format( i , df[ i ] ) , end = "\n\n" )

Aunque esto mismo podemos hacer con el método .iteritems(), donde podemos colocar dos índices, el primero es el nombre de
la
columna y el segundo es su contenido, por ejemplo:
for i , contenido in df.iteritems():
print( "Columna {}:\n{}".format( i , contenido ) , end = "\n\n" )

_________________________________________________________________________

*****2.7- Ficheros CSV y excel*****

Los ficheros csv (valores separados por coma) son archivos que contienen lo dicho en su descripción, pero podmeos cargarlos
como dataframes en python.

Para importar un CSV en Google Colab hay varias maneras. La primera de ellas es desde Drive. Para ello podemos ir al folder
del lado izquierdo en Colab y buscar el símbolo de una carpeta de Drive. Darle click y listo. También podemos hacerlo a
mano desde una celda de código escribiendo lo siguiente:
from google.colab import drive
drive.mount( "/content/drive" )

Una vez tengamos acceso a Drive, ya podemos cargar archivos csv a través de pandas con la función .read_csv( ruta ) en donde
en el argumento colocamos la ruta del archivo que queremos cargar, para ello en el explorador de la izquierda buscamos el
archivo en nuestra carpeta de Drive y damos click derecho, damos click en copiar ruta y la colcamos en el argumento de
la función como string de .read_csv(). Al momento de cargar, debemos almacenar la carga en una variable. Por ejemplo:
import pandas as pd
df = pd.read_csv( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/characters-simpsons.csv" )
df.head()
es importante destacar que pandas carga los csv como dataframe, por lo que podemos aplicarle todos los métodos vistos hasta
ahora. Siendo más explícitos, este es el argumento filepath =, por lo que podemos escribir:
df = pd.read_csv( filepath = "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/characters-simpsons.csv" )
df.head()
cualquier tipo de URL es aceptable, por ejemplo, mediante las usadas con la clase Path de pathlib, que abordamos más
adelante.

Si nosotros hemos construido un dataframe y lo queremos guardar en un archivo .csv lo podemos lograr aplicando el método
.to_csv al dataframe que queremos guardar:
df.to_csv( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/nuevo_archivo.csv" )
Al mirar el archivo creado, podemos ver que se ha cargado también los índices numerados. Para evitar esto, podemos colocar
el parámetro index = False y listo:
df.to_csv( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/nuevo_archivo.csv" , index = False )
en este caso la primera columna quedará a la izquierda.
Podems colocar el separador, colocando el parámetro sep = y especificando como cadena el separados:
dt.to_csv( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/nuevo_archivo.csv" , index = False , sep = "|" )

Si queremos cargarlo desde internet, en lugar de escribir la ruta en string en el argumento de .read_csv(), colocamos la URL
donde seguramente terminará con la extensión .csv

Para cargar csv dede GitHub necesitamos dar click en el csv y luego dar click en Raw y aparecerá el archivo en crudo,
copiamos la URL como vimos y listo.
import pandas as pd
archivo = pd.read_csv( "https://raw.githubusercontent.com/dongerard1994/r-basic/master/data/Pokemon.csv" )
archivo.head()

Si ya conocemos el dataframe, desde el inicio podemos indicar qué columna tome el papel de los índices a través del
parámetro index_col = y pasándole el número de la columna que queremos que sean los índices, dentro de .read_csv():
df = pd.read_csv( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/artists_es.csv" , index_col = 0 )
df.head()
aunque podemos pasarle un iterable.

En ocasiones los csv tiene como separador ; en lugar de coma , por lo que hay que agregar el parámetro sep = y el símbolo
que separa como cadena:
df = pd.read_csv( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/characters-simpsons.csv" , sep = ";" )
df.head()

Los csv vienen con un tipo de codificación llamado UTF-8 pero en algunas ocasiones no es así. Podemos averiguar su
codificación con la librería chardet. El siguiente código es un ejemplo de cómo averiguar la codificación:
import chardet
rawata = open( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/characters-simpsons.csv" , "rb" ).read()
result = chardet.detect( rawdata )
charenc = result[ "encoding" ]
print( charenc )
es una buena práctica averiguar la codificación antes de trabajar. Una vez sabida, se coloca en un parámetro encoding=, por
ejemplo:
df = pd.read_csv( "/content/drive/ArchivosCSV/characters-simpsons.csv", sep = ";" , encoding = "Windows-1252" )
df.head()
aunque para leer archivos con acentos se recomienda usar UTF-8

Podemos controlar la fila que funciona como encabezado a través del parámetro header = y colocándole el índice numérico de
la fila, por ejemplo:
df = pd.read_csv( "/content/drive/ArchivosCSV/characters-simpsons.csv", sep = ";" , encoding = "Windows-1252"
header = 0 )
df.head()

En ocasiones los datasets no tienen el nombre de las columnas, pero si los conociéramos, podemos agregarlos mediante el
parámetro names = dándole una lista con los nombres de las columnas:
df = pd.read_csv( "/content/drive/ArchivosCSV/characters-simpsons.csv", sep = ";" , encoding = "Windows-1252"
header = 0 , names = [ "Primera" , "Segunda" , "Tercera" , "Cuarta" ] )
df.head()
por defecto está header = None

Podemos predisponer el tipo de datos como en numpy, a través del argumento dtype =, al cual le pasamos un diccionario que
tiene por claves el nmbre de las columnas a cambiar y cmo valor el tipo de dato usando np, por defecto tiene None:
df = pd.read_csv( "/content/drive/ArchivosCSV/characters-simpsons.csv", sep = ";" , encoding = "Windows-1252"
dtype = { "Weight":np.float64 , "Age":int32 } )
df.head()

En ocasiones, las primeras filas puede que no tengan datos, por lo que al caragar el dataset, podemos saltarnos algunas
filas mediante el argumento skiprows = colocándole cuántas de las primeras filas no tomará en cuenta para cargar el dataset:
df = pd.read_csv( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/characters-simpsons.csv" , sep = ";"
skiprows = 4 )
df.head()

Podemos quitar los valores NaN mediante el parámetro skip_blank_lines = , el cual es un booleano. Al colocarle True omite
las filas que contengan valores NaN en la carga:
df = pd.read_csv( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/characters-simpsons.csv" , sep = ";"
skip_blank_lines = True )
df.head()
lo mismo sucede con na_filter = True.

Para leer archivos de excel, lo único diferente es que ahora hay que usar la función pd.read_excel( "ruta" ), recordando
que los archivos de excel tienen una estensión .xlsx

También podemos generar archivos tipo excel con la función .to_excel() aplicado a un dataframe
df.to_excel( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/nuevo_archivo.xlsx" , index = False )
Podemos controlar el nombre de la hoja a través del parámetro sheet_name = y dándole como cadena el nombre:
df.to_excel( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/nuevo_archivo.xlsx" , index = False ,
sheet_name = "Negrito_sandia" )

Excel dejará celdas vacías en las entradas donde el dataframe contenga valores nulos.

Hay diversos tipos de extensiones para guardar un dataframe, algunas extensiones tienen una ventaja al hablar de compresión
y ahorro de memoria, y de rendimiento frente a la cantidad de datos, algunos de ellos son:
-json: este lo revisamos en la siguiente sección
-pickle: Permite comprimir la información, es util cuando se tienen tablas grandes
-Parquet: Permite darle un formato que puede usarse en ambientes de Big Data como Hadoop
-hdf o h5: Permite darle un formato que puede usarse en ambientes de Big Data como Hadoop

los comandos son muy similares; .to_pickle() , pd.read_pickle() , .to_parquet() , pd.read_parquet(),


.to_hdf( ruta , key = "data" , format = "table" ) y pd.read_parquet(). Para más información de los formatos podemos ver:
https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html

Los formatos .csv y .json son los más pesados, el más ligero por mucho es el .parquet. En cuanto a carga y lectura, .csv
y .json tardan mucho, mientras que .parquet, .pickle y .hdf son los más veloces, además de tener un costo en RAM mucho
menor. En cuanto a la cantidad de datos, .parquet es el mejor junto con .pickle.

Podemos transformar una columna a formato datetime con pd.to_datetime( columna )


_________________________________________________________________________
*****2.8- Ficheros JSON*****

Los archivos JSON (Java Script Object Notation). Los archivo JSON tienen una estructura de diccionario no necesariamente
rectangular. En el contexto de los archivos JSON a los diccionarios se les llama "objetos". Con este nombre podemos decir
que los archivos JSON son estructuras que anidan objetos en más objetos o arrays. Un ejemplo de JSON es:
{
"0":{
"Producto": "Bolígrafo",
"Precio": "1.80",
"Cantidad": "3"
},

"1":{
"Producto": "Lápiz",
"Precio": "0.30",
"Cantidad": "2"
},

"2":{
"Producto": "Libreta",
"Precio": "5.20",
"Cantidad": "1"
},

"3":{
"Producto": "Agenda",
"Precio": "9.99",
"Cantidad": "1"
},

"4":{
"Producto": "Rotulador",
"Precio": "1.15",
"Cantidad": "5"
}
}
es un formato que les agrada mucho a los desarrolladores de software y creadores de páginas web.

Para cargar un JSON con pandas se hace algo similar a los csv, en este caso usamos el método .read_json(). Por ejemplo:
import pandas as pd
archivo = pd.read_json( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/json_index_example.json" ,
orient = "index" )
archivo.head()
en este caso las columnas fueron las claves de los tres objetos interiores

Lo análogo sucede si queremos cargar desde una URL.

Otra forma básica de escribir un JSON es con la siguiente sintaxis:


{
"Producto":{
"0": "Bolígrafo",
"1": "Lápiz",
"2": "Libreta",
"3": "Agenda",
"4": "Rotulador"
},

"Precio":{
"0": "1.80",
"1": "0.30",
"2": "5.20",
"3": "9.99",
"4": "1.15"
},

"Cantidad":{
"0": "3",
"1": "2",
"2": "1",
"3": "1",
"4": "5"
}
}
en este caso las claves del JSON deberán ser las columnas. Habrá que modificar ligeramente la forma de importarlos:
import pandas as pd
archivo = pd.read_json( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/json_index_example.json" ,
orient = "columns" )
archivo.head()

Otra forma más de escribir un JSON es con arrays, por ejemplo:


[
[ "Bolígrafo" , "1.80" , "3" ] ,
[ "Lápiz" , "0.30" , "2" ] ,
[ "Libreta" , "5.20" , "1" ] ,
[ "Agenda" , "9.99" , "1" ] ,
[ "Rotulador" , "1.15" , "5" ]
]
es el tipo de JSON más caótico, ni siquiera tiene el nombre de las columnas, para importarlo debemos escribir:
import pandas as pd
archivo = pd.read_json( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/json_index_example.json" ,
orient = "values" )
archivo.head()

De la misma manera podemos crear archivos json con la función .to_json():


df.to_json( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/nuevo_archivo.json" )

Trabajar con archivos JSON es complicadísimo, pero útil, por lo cual, si uno quiere importar archivos JSON más complicados
habrá que echarle un ojo a la librería beautifulsoup de Python
_________________________________________________________________________

*****2.9- Datos faltantes*****

Unos de los problemas más molestos en el análisis de datos son los datos que faltan (missing data o na) en los datasets. Hay
varios
motivos que tal vez no podamos controlar.
En Python, en particular, en pandas, hay dos tipos de missing data, los NA (Not Available) y los NaN(Not a Number). Los
primeros pueden deberse a que simplemente no fueron ingresados los datos o no se conoces y los segundos se pueden deber a
un cálculo en donde haya casos que no se consideraron y resultaron cálculos con infinitos o similares.

Para añadir valores de este tipo a un dataframe necesitamos importar numpy y colocarlos como si fueran constantes con el
método .nan.

Dado un dataframe, podemos crear otro dataframe que nos diga en donde hay o no hay valores nulos. Para ello usamos las
funciones .isnull() y .notnull(). El primero marca con True los valores nulos y el segundo marca con True los valores que no
son nulos. Por ejemplo:
archivo_bool = archivo.isnull()
archivo_bool.head()

Podemos hacer varias cosas con los datos faltantes. El método .fillna(valor) nos permite sustituir los valores faltantes por lo que
le indiquemos por parámetro. Por ejemplo, podemos sustituirlos por cero como se muestra a continuación:
archivo.fillna( 0 )
arhivo.head()
aunque lo podemos guardar para no alterar el dataframe original.
archivo_nuevo = archivo.fillna( 0 )
arhivo_nuevo.head()
si toda la fila tiene valores nan, podemos ponerle un diccionario como parámetro que contenga por claves los nombres de las
columnas y por filas los valores que queremos agregar correspondientes:
df.fillna( { "Confirmados":555.0 ,
"Muertos":17.0 ,
"Recuperados":28.0 } ,
inplace = True )
también podemos introducir una columna junto con la operación que le queremos hacer:
df[ "Confirmados" ].fillna( data[ "Confirmados" ].mean() )
podemos agregarle el parámetro method para hacer acciones más especializadas, por ejemplo, si le damos "ffill" nos rellena
el valor faltante con el valorsiguiente:
df[ "Confirmados" ].fillna( method = "ffill" )
para colcoar el valor anterior tomamos el argumento "backfill":
df[ "Confirmados" ].fillna( method = "backfill" )

Hay un método más poderoso que el .fillna(), pues es una especie de generalización. El método .replace( viejo , nuevo ) nos
permite sustituir un cierto valor de un dataframe por uno nuevo. Para ello ponemos el valor a sustituir en la primera
entrada y el nuevo valor en la segunda. Por ejemplo, si queremos quitar los valores nulos podemos colocar:
archivo_nuevo = archivo.replace( np.nan , 0 )
archivo_nuevo.head()

Podemos colocar la terminación .interpolate() para colocar interpolaciones lineales (mínimos cuadrados) en los valores nulos
archivo_nuevo = archivo.interpolate()
archivo_nuevo.head()

En ocasiones no nos convendrá conservar los valores nulos. Para eliminar las observaciones (filas) con valores nulos,
podemos utilizar el método .dropna(), por ejemplo:
archivo_nuevo = archivo.dropna()
archivo_nuevo.head()
podemos agregarle el parámetro axis = que se iguala a 0 o a 1, dependiendo si se qiere aplicar en las filas o en las
columnas respectivamente:
archivo_nuevo = archivo.dropna( axis = 0 )
archivo_nuevo.head()
de igual manera hay varias formas de restringir esta función mediante el parámetro how = , por ejemplo, se borrarán las
filas que tengan TODAS las observaciones como NaN pasándole "all".
archivo_nuevo = archivo.dropna( axis = 0 , how = "all" )
archivo_nuevo.head()
de manera análoga, el parámetro how = "any", borra toda la fila si alguna observación de ella contiene al menos un NaN:
archivo_nuevo = archivo.dropna( axis = 0 , how = "any" )
archivo_nuevo.head()

En ocasiones, las primeras filas puede que no tengan datos, por lo que al caragar el dataset, podemos saltarnos algunas
filas mediante el argumento skiprows = colocándole cuántas de las primeras filas no tomará en cuenta para cargar el dataset:
df = pd.read_csv( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/characters-simpsons.csv" , sep = ";"
skiprows = 4 )
df.head()
_________________________________________________________________________

*****2.10- Filtrado de dataframes*****

Para filtrar las filas de un dataframe y crear un dataframe con las columnas filtradas hay tres formas. La primera es crear
una serie de pandas. Al involucrar una columna de un dataframes en una expresión condicional, pandas nos devuelve una serie
que tiene por elementos booleanos, que son True si el elemento de la fila cumple la condición o False en caso contrario, por
ejemplo:
df[ "Precio" ] > 2

La segunda es llamar al subdataframe usando el nombre del dataframe original y unos corchetes donde se coloca la columna del
dataframe y la
condición que se desea imporner, por ejemplo:
archivo[ archivo[ "Precio" ] > 2 ]
nos devuelve un dataframe donde todas las filas tienen el valor de la columna "Precio" mayor a dos. Estos cambios no se
guardan en el dataframe original, hay que guardarlos manualmente en el mismo dataframe o en otro nuevo para no alterar el
original. Por ejemplo:
archivo_filtrado = archivo[ archivo[ "Precio" ] > 2 ]
dentro del slicing de archivo, podemos colocar más de una condición con los operadores lógicos como ya hemos visto antes.

La segunda forma exactamente igual a la anterior, es usar el método .loc(), a este método se le pueden colocar condicicones
como argumento y pandas lo filtrará igual, por ejemplo:
df.loc[ df[ "Precio" ] > 2 ]

La última forma consiste en usar el método .query( condición ), donde el argumento debe tener el nombre de la columna y la
condición que le aplicaremos TODO EN FORMATO STRING, por ejemplo:
archivo.query( "Precio > 2" )
el método query tiene una limitante. Las columnas del dataframe no debe tener strings que contengan espacios, lo cual es
bastante normal, no es común colocar nombres con espacios, siempre con algun case como los que se vieron al inicio.

La ventaja del método query es que es menos técnica y más fácil de leer para una persona que no esté tan familiarizado con
Python, además tiene cietra similitud con el lenguaje SQL. Además podemos escribir varias sentencias con los operadores
lógicos, por ejemplo:
archivo.query( "Precio > 2 and Precio <= 6" )

_________________________________________________________________________

*****2.11- Series de pandas*****

Hay otra clase de objeto en python conocido como serie. Es una especie de vector columna, de igual manera funcionan con
índices.
Hay dos maneras de crear una serie. Primero con una lista y creando un objeto de la clase Series(), Por ejemplo:
l=[3,4,5,6,7]
serie = pandas.Series( l )
print( serie )
nota que los índices comienzan a numerarse desde cero.

Podemos cambiar los índices indicando el parámetro "index" al crear la instancia Series(). Por ejemplo:
l = [ 3 , 4 , 5 , 6, 7 ]
serie = pandas.Series( l, index = [ "Primero" , "Segundo" , "Tercero" , " Cuarto" , "Quinto" ] )
print( serie )

Para imprimir una sola entrada de una serie, hacemos un slicing. Por ejemplo:
l=[3,4,5,6,7]
serie = pandas.Series( l , index = [ "Primero" , "Segundo" , "Tercero" , "Cuarto" , "Quinto" ] )
print( serie[ "Primero" ] )

Es necesario poner índices diferentes, pues aunque se pueden poner los mismos nombres. Python no sabrá cuál queremos usar y
al llamar a un nombre en común, nos traerá todos los valores con la clave repetida.

También podemos crear una serie a través de un diccionario y su ventaja es que las claves serán directamente los índices.
Por ejemplo:
d = { "Primero":3 , " Segundo":4 , "Tercero":5 , "Cuarto":6 , "Quinto":7 }
serie = pandas.Series( d )
print( serie )

Aún no lo comentábamos pero al tomar una columna de un dataframe y averiguar su tipo, nos llevamos la sorpresa de que es
una
serie, así que aplicarle los métodos de dataframe a una columna no nos servirá:
print( type( archivo[ "Primera" ] ) )
pero podemos convertir esta columna a una lista (donde se pierden los índices), un diccionario, o un dataframe con los
métodos y funciones list(), dict() o .tolist(), .toframe() sin argumento el método, respectivamente

Los métodos de los dataframes pueden ser aplicados a los tipo Series de pandas, pero solo los que tienen sentido.
_________________________________________________________________________

*****2.12- índice multinivel o Multiíndices de pandas*****

Es bastante común que dada una lista de datos recolectadas en un dataframe, haya muchos datos que se repitan. Por ejemplo
si una compañía encuesta a 10000 personas y les pide su género, su estado, su ciudad, su alcaldía, su nombre y su edad,
habrá muchísimos datos repetidos, incluso hasta los nombres pueden repetirse. Un dataframe puede transformarse en un
conjunto de datos agrupados (mapa de llaves, mapa conceptual, arbol) usando multiíndices, índices multinivel o índices
jerárquicos. Para ello tomamos un dataframe y le aplicamos el método .set_index() y como argumento una lista con los nombres
de las columnas en el orden que queramos tomar las llaves o la jerarquía y la guardamos en una variable más:
df_agrupado = df.set_index( [ "Quinta" , "Primera" , "Tercera" , "Cuarta" , "Segunda" ] )
este nuevo objeto df_agrupado será un dataframe con datos agrupados. Es decir, ya no accederemos a los datos mediante la
fila y la columna, sino que accederemos a ellos indicando sus datos observados, como si se tratara de un arbol en
matemáticas.

Podemos verificar el nombre de los multiíndices plantados con la propiedad .index.names

Para ver los valores que guardan todos los índices colocamos la propiedad .index.values

Es común que a pesar de crear multiíndices, python no siempre los reagrupe de manera correcta. Esto se debe a que el método
internamente se vuelve ineficiente cuando los datos no están ordenados. Esto lo podemos remediar colocando el método
.sort_index() al crear los multiíndices como se muestra a continuación:
df_agrupado = df.set_index( [ "Quinta" , "Primera" , "Tercera" , "Cuarta" , "Segunda" ] ).sort_index()

Para deshacer el orden jerarquico podemos usar el método .reset_index():


df_agrupado.reset_index()

Para consultar ahora nuestro dataframe con multíndices nuevamente usamos los dos parámetros con .loc[], el primero es para
las filas y el segundo para las columnas. Nuevamente podemos colocar solo el nombre de la clave del dataframe (los índices ya
no), una tupla con las columnas a consultar o los puntos : en caso de que se requieran todas las filas o todas las columnas,
dependiendo en dónde se coloque.
df_agrupado.loc[ ( Llave1 , Llave2 ) , : ]

Podemos saltarnos (o invalidar) llaves del orden jerárquico colocando como parámetro slice(None):
df_agrupado.loc[ ( Llave1 , slice( None) , Llave3 ) , : ]

Podemo hacer una busqueda de manera más sencilla, podemos usar el método .xs( Llave, level = Columna ) donde el primer
parámetro es la llave (índice multinivel) que queremos buscar y el segundo parámetro es el nombre de la columna original.
df.xs( Llave , level = Columna )
por ejemplo:
df.xs( [ "Colombia" , "2018" ] )
si nos queremos concentrar solo en "2018", habrá que indicar el nivel:
df.xs( "2018" , level = "año" )

Mediante la clase pd.IndexSlice podemos filtrar búsquedas multiíndices, por ejemplo, queremos todos los datos desde colombia
hasta méxico y solo del año 2015 al 2018. Primero hay que ordenar en orden alfabético ls paises:
df = df.set_index( [ "País" , "Año"] ).sort_values( ascending = [ True , True ] )
luego filtramos:
df.loc[ pd.IndexSlice[ "Colombia":"México , "2015":2018 ] , : ].sort_index()

Para obtener una lista de los índices multinivel usamos .index.get_level_values( numero ) y como argumento le colocamos un
número que indica la posición en la que definimos nuestros índices multinivel, en el ejemplo anterior fue con la lista
[ "País" , "Año" ]:
df.index.get_values( 1 )
esto nos da los años.

El método .unstack( nombre ) lleva el nombre de un índice multinivel y lo convierte en columna:


df.unstack( "Año" )

_________________________________________________________________________

*****2.13- Tablas pivote o tablas dinámicas*****

Una tabla pivote o tabla dinámica es una tabla que resume resultados de otra tabla mucho más grande, es conveniente usarlas
cuando debemos tratar una gran cantidad de datos. En estas tablas, se cuentan, suman, promedian etc, los datos asociados
a un grupo de registros dentro de la tabla base. Se pueden construir de varias maneras, las principales son, con el método
.groupby() y con el método .pivot_table(), ambos por supuesto, aplicados al datarame en cuestión. La ventaja de pandas es
que, con otras librerías se realizan para cada par de variables column, mientras que pandas permite hacer las tablas para
más de dos variables de interés.

Los valores que se agregarán en las filas y columnas se calculan a través de ciertos métodos aplicados a la tabla pivote,
conocidos como "funciones de agregación".
Consideremos el siguiente dataframe:
datos = { "nombre" : [ "Lambda" , "Snoopy" , "Kisame" , "Otto" , "Remy" , "Molly" ],
"tamaño" : [ "grande" , "mediano" , "grande" , "grande" , "pequeño" , "pequeño" ],
"sexo" : [ "F" , "M" , "M" , "M" , "M" , "F" ],
"peso" : [ 20.0 , 8.0 , 27.0 , 17.0 , 3.0 , 3.5 ],
"edad" : [ 7 , 2 , 6 , 3 , 7 , 7 ] }

df = pd.DataFrame( datos )
luego, a un subdataframe de interés, por ejemplo df[ [ "tamaño" , "edad"] ] o incluso al dataframe completo, le aplicamos la
función .groupby() cuyo argumento es un string con el nombre de la columna con la que queremos que se agrupe. Esto por sí
solo no nos arroja nada, hay que aplicarle una función al final, la función de agregación. Hay varias, por colocar un
ejemplo, está la función promedio .mean(), .count(), .min(), .max(), .sum(), .prod() , .std() etcétera:
df[ [ "tamaño" , "edad" ] ].groupby( "tamaño" ).mean()
en este caso, los índices serán los tamaños, mientras que los valores, nos muestran el promedio de su edad por tamaño.
También podermos pasarle por parámetro la columna del dataframe en lugar del string:
df[ [ "tamaño" , "edad" ] ].groupby( df[ "tamaño" ] ).mean()
Podemos ordenar tablas dinámicas o incluso series mediante la función .sort_values(), el primer argumento es la columna por
la que queremos agregar, podemos también ordenar de manera ascendente o descendente mediante el parámetro ascending =
df[ [ "tamaño" , "edad" ] ].groupby( "tamaño" ).mean().sort_values( "tamaño" , ascending = False )

Podemos agregar una función de agregación que actúe distinto en cada fila, para ello, la función es .agg( diccionario ) que
contendrá un diccionario que tiene por claves el nombre de las columnas y por valor, la función de agregación en un string
y sin argumentos, por ejemplo:
df[ [ "tamaño" , "edad" , "peso" ] ].groupby( "tamaño" ).agg( { "edad":"mean" , "peso":"sum" } )

Podemos tomar dos índices y segmentar como un árbol pasándole a .groupby() una lista con los índices a agrupar:
df.groupby( [ "tamaño" , "sexo" ] ).agg( { "edad":"mean" , "peso":"sum" } )

Podemos aplicar las mismas funciones de agregación pasándoselas a .agg(9 como una lista, de la siguiente manera:
df.groupby( [ "tamaño" , "sexo" ] ).agg( [ "mean" , "sum" ] )

Podemos mezclar estas dos sintaxis anteriores e indicar por separado para cada columna, varias funciones de agregación
colocando en un diccionario el nombre de la columna como clave y las funciones de agregación como una lista:
df.groupby( [ "tamaño" , "sexo" ] ).agg( { "edad":[ "mean" , "sum" ] , "peso":"max" } )

También podemos usar funciones customizadas para colocarlas como funciones de agregación, ya sea una función estándar o
una
función lambda. Las funciones deben estar definidas como una función que toma un array y devuelve un escalar (aunque se
puede jugar con el objeto que devuelven):
def norma( x ):
return ( x**2 ).sum()

df2.groupby( "Ultima_letra" ).agg( [ norma ] )

Podemos extraer un subdataframe de un groupby haciendo nuevamente slicing con corchetes, por ejemplo:
df.groupby( [ "Primera_letra" , "Ultima_letra" ] )[ [ "Palindromo" ] ]

La función .groupby() nos permite iterar con dos índices, el primero es la categoría con la cual se realizó la agrupación,
la segunda es el subdataframe cuyos datos están en la categoría:
for categoria , subframe in df2.gropuby( "Ultima_letra" ):
print( "La categoría es: {}\n el subdataframe asociado es:".format( categoria ) )
print( subframe )
Las tablas agrupadas nos permiten aplicarles el método .describe(). Es cómodo primero seleccionar una variable con un
slicing y aplicarle .describe()

Podemos hacer un conteo definiendo intervalos de una columna con la función pd.cut( columna , bins = ) en donde le pasamos
una columna de un groupby y el parámetro bins = con el número de divisiones que queremos que se haga:
pd.cut( df[ "pago" ] , bins = 3 ).value_counts()
podemos escribir a manos los números de la división en forma de una lista:
df[ "bin_total" ] = pd.cut( df[ "pago" ] , bins = [ 3 , 18 , 35 , 60 ] )
en este caso se creará una columna que tiene el intervalo al cual pertenece el dato.

Si agrupamos de acuerdo a una columna, podemos quedarnos con un dataframe de uno de los grupos mediante el método
.get_group( "nombre_categoria" ) aplicándosela a un dataframe ya agrupado:
df_gender = df.groupby( "gender" )
df_gender.get_group( "Female" )
Si se trata de una agrupación con varias columnas, podemos ver un grupo dánole una tupla con las categorías que queremos
visualizar:
df_sex = df.groupby( [ "sex" , "pclass" ] )
df_sex.get_group( ( "female" , 1 ) )
podemos obtener un diccionario con los grupos y TODOS los valores asociados a cada grupo mediante el atributo .groups, sin
embargo, esto no es práctico, pero podemos usarlo para averiguar todos los grupos gracias a los métodos de diccionario:
df_sex.groups.keys()

Otra forma de filtrar datos en los groupby es a través del método .filter() justo como ya lo conocíamos:
df_filtrado[ "age" ].filter( lambda x : x.sum() > 2400 )

Otra forma de generar nuevos datos es a través del método .transform(), que funciona básicamente igual que el .apply() pero
para datos agrupados:
zscore = lambda x : (x - x.mean() ) / x.std()
df.transform( zscore )

Podemos calcular cuántas observaciones hay en los grupos mediante una tabla agrupada sin función de agregación mediante el
método .size():
df_sex.size()

La función .pivot_table() aplicada a un dataframe, nos permite hacer lo mismo y de una manera muy parecida. Nuevamente
consideremos el dataframe de perros que hicimos antes. En esta ocasión, las columnas a tomar como índice, la columna con
los valores a considerar y la función de agregación se colocan como argumentos de .pivot_table(), por ejemplo:
df.pivot_table( index = "tamaño" , values = "edad" , aggfunc = "mean" )
nos genera la misma tabla dinámica que hicimos antes.

Podemos tomar dos índices y segmentar como un árbol pasándole a .pivot_table() una lista con los índices a agrupar:
df.pivot_table( [ "tamaño" , "sexo" ] , values = "edad" , aggfunc = "mean" )

Podemos colocar una de las columnas del dataframe original a través del parámetro columns =, por ejemplo:
df.pivot_table( index = "tamaño" , columns = "sexo" , values = "edad" , aggfunc = "mean" )

En el fondo una pivot_tables es una tabla multiíndice, por lo que para acceder a sus registros necesitamos especificar los
valores de las llaves como diagrama de árbol:
df.pivot_table( [ "tamaño" , "sexo" ] , values = "edad" , aggfunc = "mean" )
df.loc[ "grande" , "M" ]
de la misma forma podemos colocar una lista de funciones de agregación en una lista en el parámetro aggfunc
Finamente, podemos crear una "tabla cruzada" entre dos columnas usando la función pd.crosstab( serie1 , serie2 ) con las
columnas a seleccionar:
pd.crosstab( df[ "tamaño" ] , df[ "edad" ] )
esta tabla cruzada no es más que colocar las columnas como ejes de un dataframe y en sus valores las frecuencias.

_________________________________________________________________________

*****2.14- Concatenación de dataframes*****

Podemos combinar dos o más dataframes, comenzaremos viendo las fusiones más obvias e incrementaremos la dificultad.
Supongamos que hemos definido dos dataframes de la siguiente forma:
df1 = pd.DataFrame( { A:[ "A0" , "A1" , "A2" , "A3" ]
B:[ "B0" , "B1" , "B2" , "B3" ]
C:[ "C0" , "C1" , "C2" , "C3" ]
D:[ "D0" , "D1" , "D2" , "D3" ]
})

df2 = pd.DataFrame( { A:[ "A4" , "A5" , "A6" , "A7" ]


B:[ "B4" , "B5" , "B6" , "B7" ]
C:[ "C4" , "C5" , "C6" , "C7" ]
D:[ "D4" , "D5" , "D6" , "D7" ]
})
esto nos dará los dataframes:
|A |B |C |D | |A |B |C |D |
------------------------ ------------------------
0 | A0 | B0 | C0 | D0 | 0 | A4 | B4 | C4 | D4 |
------------------------ ------------------------
1 | A1 | B1 | C1 | D1 | 1 | A5 | B5 | C5 | D5 |
------------------------ ------------------------
2 | A2 | B2 | C2 | D2 | 2 | A6 | B6 | C6 | D6 |
------------------------ ------------------------
3 | A3 | B3 | C3 | D3 | 3 | A7 | B7 | C7 | D7 |

Podemos fusionar de dos formas los dataframes, primero comenzaremos usando la función pd.concat( lista , axis = numero ) en
donde se le pasa una lista con los dataframes y el índice por donde se quiera fusionar, por ejemplo, para aumentar el
número de filas usamos el axis 0:
pd.concat( [ df1 , df2 ] , axis = 0 )
esto nos mostrará el dataframe:
|A |B |C |D |
------------------------
0 | A0 | B0 | C0 | D0 |
------------------------
1 | A1 | B1 | C1 | D1 |
------------------------
2 | A2 | B2 | C2 | D2 |
------------------------
3 | A3 | B3 | C3 | D3 |
------------------------
0 | A4 | B4 | C4 | D4 |
------------------------
1 | A5 | B5 | C5 | D5 |
------------------------
2 | A6 | B6 | C6 | D6 |
------------------------
3 | A7 | B7 | C7 | D7 |
nota que los índices se repitieron, para evitarlo agragamos el parámetro ignore_index = True
pd.concat( [ df1 , df2 ] , axis = 0 , ignore_index = True )
entonces el dataframe quedará:
|A |B |C |D |
------------------------
0 | A0 | B0 | C0 | D0 |
------------------------
1 | A1 | B1 | C1 | D1 |
------------------------
2 | A2 | B2 | C2 | D2 |
------------------------
3 | A3 | B3 | C3 | D3 |
------------------------
4 | A4 | B4 | C4 | D4 |
------------------------
5 | A5 | B5 | C5 | D5 |
------------------------
6 | A6 | B6 | C6 | D6 |
------------------------
7 | A7 | B7 | C7 | D7 |
otra forma de hacer esto mismo de pegar uno debajo del otro se realiza con la función para listas .append() aplicada a un
dataframe y colocando como argumento un segundo dataframe:
df1.append( df2 )
para unir a lo largo usamos la traspuesta:
df1.append( df2.T ).T

Si queremos juntar todas las columnas, usamos el otro eje:


pd.concat( [ df1 , df2 ] , axis = 0 )
esto nos mostrará el dataframe:
|A |B |C |D |A |B |C |D |
--------------------------------------------
0 | A0 | B0 | C0 | D0 | A4 | B4 | C4 | D4 |
--------------------------------------------
1 | A1 | B1 | C1 | D1 | A5 | B5 | C5 | D5 |
--------------------------------------------
2 | A2 | B2 | C2 | D2 | A6 | B6 | C6 | D6 |
--------------------------------------------
3 | A3 | B3 | C3 | D3 | A7 | B7 | C7 | D7 |

Podemos hacer otra fusión a través del comando .merge() aplicado a un dataframe y se le pone como argumento otro
dataframe,
el dataframe que se concatena a la izquierda es al que se le aplica el método, mientras que el de la derecha es el
argumento. Este método toma los valores de las filas que coinciden y los concatenan, a esto se le conoce como un innerjoin.
Por ejemplo, los dataframe:
izquierdo = pd.DataFrame( { key:[ "k0" , "k1" , "k2" , "k3" ]
A:[ "A0" , "A1" , "A2" , "A3" ]
B:[ "B0" , "B1" , "B2" , "B3" ]
})

derecho = pd.DataFrame( { key:[ "k0" , "k1" , "k2" , "k3" ]


C:[ "C4" , "C5" , "C6" , "C7" ]
D:[ "D4" , "D5" , "D6" , "D7" ]
})
los podemos concatenar sin repetir la columna "key" mediante:
izquierdo.merge( derecho , on = "key" )
quedándo el dataframe siguiente:
| key | A | B | C | D |
------------------------------
0 | k0 | A0 | B0 | C0 | D0 |
------------------------------
1 | k1 | A1 | B1 | C1 | D1 |
------------------------------
2 | k2 | A2 | B2 | C2 | D2 |
------------------------------
3 | k3 | A3 | B3 | C3 | D3 |
podemos poner más parámetros colocándolos en una lista en el parámetro on =, aunque pandas detecta esto por defecto.

Si no coinciden las columnas de los dataframes , en los nombres como:


izquierdo = pd.DataFrame( { key:[ "k0" , "k1" , "k2" , "k3" ]
A:[ "A0" , "A1" , "A2" , "A3" ]
B:[ "B0" , "B1" , "B2" , "B3" ]
})

derecho = pd.DataFrame( { key2:[ "k0" , "k1" , "k2" , "k3" ]


C:[ "C4" , "C5" , "C6" , "C7" ]
D:[ "D4" , "D5" , "D6" , "D7" ]
})
los podemos concatenar en el orden haciendo:
izquierdo.merge( derecho , left_on = "key" , right_on = "key2" )
quedando:
| key | A | B | key2 | C | D |
-------------------------------------
0 | k0 | A0 | B0 | k0 | C0 | D0 |
-------------------------------------
1 | k1 | A1 | B1 | k1 | C1 | D1 |
-------------------------------------
2 | k2 | A2 | B2 | k2 | C2 | D2 |
-------------------------------------
3 | k3 | A3 | B3 | k3 | C3 | D3 |
si en la columna con la que estamos concatenando, no se encuentra un valor, .merge() no los devuelven por ejemplo, los
dataframes:
izquierdo = pd.DataFrame( { key:[ "k0" , "k1" , "k2" , "k3" ]
A:[ "A0" , "A1" , "A2" , "A3" ]
B:[ "B0" , "B1" , "B2" , "B3" ]
})

derecho = pd.DataFrame( { key2:[ "k0" , "k1" , "k2" , np.nan ]


C:[ "C4" , "C5" , "C6" , "C7" ]
D:[ "D4" , "D5" , "D6" , "D7" ]
})
quedan concatenados de la siguiente manera:
| key | A | B | key2 | C | D |
-------------------------------------
0 | k0 | A0 | B0 | k0 | C0 | D0 |
-------------------------------------
1 | k1 | A1 | B1 | k1 | C1 | D1 |
-------------------------------------
2 | k2 | A2 | B2 | k2 | C2 | D2 |

Podemos cambiar esto dándole más importancia a uno de los dataframes, introduciendo el argumento how = y alguna de las
opciones "inner", "left", "right" y "outer". Por defecto está el valor "inner", pero podemos hacer un left-join haciendo:
izquierdo.merge( derecho , left_on = "key" , right_on = "key2" , how = "left" )
quedando el dataframe:
| key | A | B | key2 | C | D |
---------------------------------------
0 | k0 | A0 | B0 | k0 | C0 | D0 |
---------------------------------------
1 | k1 | A1 | B1 | k1 | C1 | D1 |
---------------------------------------
2 | k2 | A2 | B2 | k2 | C2 | D2 |
---------------------------------------
3 | k3 | A3 | B3 | NaN | NaN | NaN |
podemos darle preferencia al dataframe derecho (right-join) haciendo:
izquierdo.merge( derecho , left_on = "key" , right_on = "key2" , how = "right" )
quedando el dataframe:
| key | A | B | key2 | C | D |
-------------------------------------
0 | k0 | A0 | B0 | k0 | C0 | D0 |
-------------------------------------
1 | k1 | A1 | B1 | k1 | C1 | D1 |
-------------------------------------
2 | k2 | A2 | B2 | k2 | C2 | D2 |
-------------------------------------
3 | NaN | NaN | NaN | NaN | C3 | D3 |

Podemos usar de la misma forma merge como una función de pandas y no como un método de dataframes, es decir, usar la
función
pd.merge() y pasarle una lusta con los dataframes que queremos que concatene:
pd.merge( [ df1 , df2 ] )
le podemos colocar casi los mismos argumentos que al método .merge(), como el how = , on = , left_on y right_on = .
pd.merge( [ df1 , df2 ] , left_on = "key" , right_on = "key2" , join = "left" )
Cuando tenemos dos coulmans que contienen índices compartidos pero tienen datos que conflictuan el merge, pandas le coloca
un sufijo al nombre de las colummnas, por ejemplo "A_x" y "A_y". Si no nos gusta el sufijo que les coloca por defecto,
podemos pedirle que agregue otros sufijos mediante el parámetro suffixes = y le pasamos una lista:
pd.merge( [ df1 , df2 ] , left_on = "key" , right_on = "key2" , join = "left" ,
suffixes = [ "_left " , "_right" ] )

Finalmente, podemos hacer exactamente lo mismo con el método .join() solo que este método trabaja solo con los índices, y no
con columnas específicas (lo cual es una pequeña limitante), por ejemplo, los dataframes:
izquierdo = pd.DataFrame( { A:[ "A0" , "A1" , "A2" ] ,
B:[ "B0" , "B1" , "B2" ]
},
index:[ "k0" , "k1" , "k2" ] )
derecho = pd.DataFrame( { C:[ "C0" , "C1" , "C2" ] ,
D:[ "D0" , "D1" , "D2" ]
},
index:[ "k0" , "k2" , "k3" ] )
se ven:
|A |B | |C |D |
--------------- ---------------
k0 | A0 | B0 | k0 | C0 | D0 |
--------------- ---------------
k1 | A1 | B1 | k2 | C1 | D1 |
--------------- ---------------
k2 | A2 | B2 | k3 | C2 | D2 |
al concatenarlos mediante el código:
izquierdo.join( derecho , how = "inner" )
nos da la siguiente tabla:
|A |B |C |D |
-------------------------
k0 | A0 | B0 | C0 | D0 |
-------------------------
k2 | A2 | B2 | C1 | D1 |
como vemos, funciona exactamente igual
_________________________________________________________________________

*****2.15- Conexión con bases de datos tipo SQL*****

Ver Curso de Manipulación y Análisis de Datos con Pandas y Python 2020


David Torres
_________________________________________________________________________

*****2.16- Series de tiempo en pandas*****

Ver Curso de Manipulación y Análisis de Datos con Pandas y Python 2020


David Torres
_________________________________________________________________________________________________________
____________________

Capítulo 3: Archivos importados en Python

*****3.1 Archivos txt*****

Los archivos de texto plano tienen una extensión .txt

Para cargar un txt usamos la función open() de Python. En el argumento colocamos la ruta en string ya sea de Drive o el
link, y guardamos lo que la función nos devuelve en una variable
f = open( "/content/drive/MyDrive/python-basico/datasets/first_read.txt" )
si se trata de un archivo, podemos poner simplemente la ruta o el nombre desde donde estamos si el archivo se encuentra en
la misma carpeta. Si no, hay que colocar la ruta completa. Por convención, en los trabajos profesionales, los rutas no
deberían ser tan largas, pues todo el proyecto debería localizarse en una carpeta.

para acceder al contenido del open() guardado en f, usamos la función .read()


print( f.read() )
Es necesario cerrar la consulta del txt para no corromper el contenido usando .close()
f.close()

Otra forma de abrir el archivo y guardarlo es con la palabra reservada with:


with open( "/content/drive/MyDrive/python-basico/datasets/first_read.txt" ) as f :
print( f.read() )
es una forma más segura, pues no necesita cerrarse. Además este comando intenta abrir el archivo dándonos una explicación
en caso de que no se pueda y sin corromper el archivo. Es algo que llamamos una "excepción". Los procesos que se quieran
realizar sobre el txt deben ser indentados.

Para leer parcialmente un archivo, podemos indicárselo al método .read( n ) colocando un parámetro n que significa el número
de caracteres que va a mostrar, los primeros n caracteres.

La función open() solo genera un puntero, no carga información, es decir, si tenemos un txt de 70gb, al abrir con open() en
f, no se cargan los 70gb, no se satura la memoria.

El método .readline() es similar a .read(), si lo colocamos sin argumento nos permite leer una línea. Podemos leer tantas
líneas como .readline() usemos en el código:
with open( "/content/drive/MyDrive/python-basico/datasets/first_read.txt" ) as f :
print( f.readline() )
podemos colocarle un número entero como argumento a .readline( n ), los que nos permite leer los n caracteres de la línea
donde nos encontramos. Si volvemos a usar una vez más .readline( m ), seguira leyendo desde dónde se quedó, no salta a la
siguiente línea. Si usamos .readline() más veces que el número de lineas del txt, no pasa nada, Python imprime espacios en
blanco.

_________________________________________________________________________

*****3.2 Ceando txt desde Python*****

Para crear un txt desde Python, colocamos un segundo argumento en la función open(). A saber mode = "w" (Por defecto la
función open() tiene mode = "r" ). Colocamos el nombre de la ruta donde lo vamos a crear y lo completamos con /nombre.txt ,
f = open( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/nuevo.txt" , mode = "w" )
f.close()
si ya existe un archivo con el nombre, lo machacaremos y crearemos uno nuevo.

Para escribir sobre el archivo usamos la función .write( "cadena" ) y como argumento colocamos el string con lo que queremos
escribir:
f = open( "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/nuevo.txt" , mode = "w" )
f.write( "Hola amigos.\n¿Cómo están?" )
f.close()

Los txt, Python los pasa como iterables, en donde cada iteración es una línea, por lo que podemos escribir todas las líneas
sin usar tantas veces el método .readline(), de la siguiente manera: Primero entramos en el método "r" y luego :
with open( "/content/drive/MyDrive/python-basico/datasets/first_write.txt" ) as f :
for line in f:
print( line )

El método .readlines() aplicado al archivo cargado y sin argumentos, genera una lista que contienen a las filas del txt como
elementos. Podemos iterar con este método o aplicarle cualquier método de listas.

Podmeos crear un txt vacío indicando mode = "x" y creándolo como lo hicimos con el modo "w". Nuevamente, el archivo creado
no debe existir o Python nos lanzará un error indicando que ya existe el archivo.
f = open( "/content/drive/MyDrive/python-basico/datasets/first_empty.txt", mode = "x" )
f.close()
_________________________________________________________________________

*****3.3 Sobreescribiendo un txt*****

Para sobreescribir un fichero debemos usar mode = "a" (de "actualización" como nemotecnia). Para modificar el archivo
nuevamente usamos .write(), solo que en este modo no machacaremos nada. En ocasiones es necesario comenzar con un salto
de línea pues comenzamos a agregar texto desde el siguiente espacio:
f = open( "/content/drive/MyDrive/python-basico/datasets/first_write.txt", mode = "a" )
f.write( "\nY esta última línea se ha añadido posteriormente." )
f.close()

El método .writelines( lista ) aplicado al archivo cargado, nos permite introducir una lista con palabras que queremos
agregar siempre y cuando estemos en mode = "w", aunque no es muy útil. Estas palabras las agrega todas juntas en una sola
línea, el nombre de este método es confuso. Este método no es muy práctico.

El mode "r+" nor permite tanto leere como escribir sobre el archivo, sin machacar el original, sino agregando lo nuevo al
final.

_________________________________________________________________________

*****3.4 Cargando .CSV con open()****

Primero importamos la librería csv:


import csv

Para leer un csv podemos de igual forma que con un txt, usar la función open() exactamente de la misma manera, solo que
para iterar en las columnas de un csv será necesario crear una referencia y un iterador de las filas. Esto lo logramos
usando el método csv.reader() y asinándolo a una variable. Luego al iterar accederemos a las folas en forma de listas y con
las entradas separadas por comas. Por ejemplo:
with open( "/content/drive/MyDrive/python-basico/datasets/csv_example.csv" , "r" ) as f :
reader = csv.reader( f )
for row in reader :
print( row )

La función csv.reader() también nos permite manejar el separador mediante su parámetro delimeter =, ya que no simpre están
separados por comas, por ejemplo:
with open( "/content/drive/MyDrive/python-basico/datasets/csv_delimiter_example.csv", "r" ) as f :
reader = csv.reader( f , delimiter = "|" )
for row in reader :
print( row )
es común que esto suceda cuando los elementos del csv sean strings con comas y estas puedan causar errores.

Es común que los csv se preparen con espacios después de la coma para que no se vean amontonados, pero al caragarlos estos
espacios pueden aparecer en los valores del csv. Para quitarlos usamos el parámetro skipinitialspace = True para quitarlos:
with open( "/content/drive/MyDrive/python-basico/datasets/csv_spaces_example.csv" , "r" ) as f :
reader = csv.reader( f , skipinitialspace = True )
for row in reader :
print( row )

Con varios ejemlos de csv cargados, podemos darnos cuenta que por defecto, al iterar sobre las filas de un csv, Python las
regresa como strings todas. Para evitar esto podemos agregar el parámetro quoting= con alguno de los siguientes comandos:
csv.QUOTE_ALL: indica al objeto reader que todos los valores en el archivo csv están entre comillas
csv.QUOTE_MINIMAL: indica al objeto reader que los valores en el archivo csv que están entre comillas son entradas
que contienen caracteres como el delimitador, comillas o cualquier caracter de terminación de línea
csv.QUOTE_NONNUMERIC: indica al objeto reader que los valores en el archivo csv que están entre comillas son
entradas que contienen entradas no-numéricas
csv.QUOTE_NONE: indica al objeto reader que ninguno los valores en el archivo csv están entre comillas
Los más utilizados son el csv.QUOTE_ALL(por defecto) y el csv.QUOTE_NONNUMERIC. Por ejemplo:
with open( "/content/drive/MyDrive/python-basico/datasets/csv_quotation_example.csv", "r" ) as f :
reader = csv.reader( f , quoting = csv.QUOTE_NONNUMERIC )
for row in reader :
print( row )

Si en nuestro código necesitamos ocupar muchas veces la misma forma de leer un csv, es decir, los mismos parámetros dentro
del csv.reader(), podemos preparar estos mismos parámetros de una forma corta y ahorrarnos código creando un "dialecto",
que no es más que un conjunto de parámetros. Para crear un csv usamos la función csv.register_dialect("nombre", argumentos)
Por ejemplo:
csv.register_dialect( "my_dialect", delimiter = "|" , skipinitialspace = True, quoting = csv.QUOTE_NONNUMERIC )
luego, lo agregamos al csv.reader() con el parámetro dialect
with open( "/content/drive/MyDrive/python-basico/datasets/csv_dialect_example.csv" , "r" ) as f :
reader = csv.reader( f , dialect = "my_dialect" )
for row in reader :
print( row )
los dialectos no quedan guardados en python, por lo que hay que tenerlos guardados en un fichero

Podemos transformar un csv a un diccionario, para ello usaremos el método .DictReader() del módulo csv, lo que nos
devolverá un objeto OrderedDict, que es iterable.
with open( "/content/drive/MyDrive/python-basico/datasets/csv_example.csv", "r" ) as f :
reader = csv.DictReader( f )
for row in reader :
print( row )
esto nos devolverá archivos OrderedDict con tuplas que contienen la columna acompañada del dato.

Para transformar ahora a diccionario, tranformamos cada fila con dict


with open( "/content/drive/MyDrive/python-basico/datasets/csv_example.csv" , "r" ) as f :
reader = csv.DictReader( f )
for row in reader :
print( dict( row ) )

Para crear y escribir un csv usamos la función open() junto al método csv.writer() y una lista de listas. Si queremos
crearlo fila a fila iteramos sobre la lista y la adjuntamos con la función .writerow() aplicada al escritor:
data = [[ "id" , "Name" , "City" , "Age" ] ,
[ 1234 , "Arturo" , "Madrid" , 22 ] ,
[ 2345 , "Beatriz" , "Barcelona" , 25 ] ,
[ 3456 , "Carlos" , "Sevilla" , 18 ] ,
[ 4567 , "Dolores" , "Cuenca" , 34 ] ]
with open( "/content/drive/MyDrive/python-basico/datasets/csv_write.csv" , "w" ) as f :
writer = csv.writer( f )
for row in data :
writer.writerow( row )

Para crearla de golpe usamos la función .writerrows(lista) aplicada al escritor:


with open( "/content/drive/MyDrive/python-basico/datasets/csv_write_writerows.csv" , "w" ) as f :
writer = csv.writer( f )
writer.writerows( data )
Para crearla fila a fila a partir de un diccionario usamnos la función csv.DictWriter(). Primero hay que extraer el nombre
de las columnas agragándolas en una lista como cabecera.
data = [{ "id":1234 , "Name":"Arturo" , "City":"Madrid" , "Age":22 } ,
{ "id":2345 , "Name":"Beatriz" , "City":"Barcelona" , "Age":25 } ,
{ "id":3456 , "Name":"Carlos" , "City":"Sevilla" , "Age":18 } ,
{ "id":4567 , "Name":"Dolores" , "City":"Cuenca" , "Age":34 } ]

# La cabecera es la lista de las keys de cualquiera de las entradas de data


header = list( data[ 0 ].keys() )
la agregamos al csv con el método .writeheader() aplicado al csv abierto:
with open( "/content/drive/MyDrive/python-basico/datasets/csv_write_dict.csv" , "w" ) as f :
writer = csv.DictWriter( f , fieldnames = header )
writer.writeheader()
for d in data:
writer.writerow( d )

Para hacerlo de golpe usamos .writerows() aplicado al diccionario:


with open( "/content/drive/MyDrive/python-basico/datasets/csv_write_dict_writerows.csv" , "w" ) as f:
writer = csv.DictWriter( f , fieldnames = header )

writer.writeheader()
writer.writerows( data )

También podría gustarte