1 Objetos básicos de Python

Este bloque muestra cómo crear y manipular distintos tipos de objetos básicos en Python: tuplas, listas, diccionarios, conjuntos y funciones personalizadas. También se presentan algunos ejemplos de comprensión de listas y funciones que retornan múltiples valores.

import os
import rootbc as r
from datetime import datetime as dt

# Tupla
tup = 1, 2, 3
tup 
## (1, 2, 3)
# Iterar sobre una lista de tuplas
seq = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
for a, b, c in seq:
   print('a={0}, b={1}, c={2}'.format(a, b, c))
## a=1, b=2, c=3
## a=4, b=5, c=6
## a=7, b=8, c=9
# Lista y slicing
lista = [4, 5, 6]
lista
## [4, 5, 6]
lista[0:1]
## [4]
# Diccionario y acceso/modificación
dc = {'a': 'hello', 'b': [1, 2, 3]}
dc
## {'a': 'hello', 'b': [1, 2, 3]}
dc[4] = 'good bye'
dc
## {'a': 'hello', 'b': [1, 2, 3], 4: 'good bye'}
dc['b']
## [1, 2, 3]
# Conjuntos (eliminan duplicados)
{2, 2, 2, 1, 3, 3}
## {1, 2, 3}
set([2, 2, 2, 1, 3, 3])
## {1, 2, 3}
# Comprensión de listas
# Aplica una función a los elementos de una lista si cumplen alguna condición.
# Funciona como un filtro
strings = ['a', 'as', 'bat', 'car', 'dove', 'python']
[x.upper() for x in strings if len(x) > 2]
## ['BAT', 'CAR', 'DOVE', 'PYTHON']
# Definición de una función simple
def suma(x, y):
   return x + y

suma(2, 5)
## 7
# Función con múltiples outputs
def suma2(x, y):
   s = x + y
   m = x * y
   return s, m 

a, b = suma2(2, 5)
a
## 7
b
## 10

2 Estructuras de control: Loops y Condicionales

Este bloque muestra cómo utilizar bucles for-in y while, junto con estructuras condicionales if, elif, else para controlar el flujo del programa.

# Bucle for simple
a = [0, 1, 2, 3, 4]
for x in a:
    print(x)
## 0
## 1
## 2
## 3
## 4
# Condicionales en un for
for x in a:
    if x % 2 == 0:
        print('{0} is even'.format(x))
    else:
        print('{0} is odd'.format(x))
## 0 is even
## 1 is odd
## 2 is even
## 3 is odd
## 4 is even
# Uso de elif
for x in a:
    if x == 0:
        print('{0} is zero'.format(x))
    elif x % 2 == 0:
        print('{0} is even'.format(x))
    else:
        print('{0} is odd'.format(x))
## 0 is zero
## 1 is odd
## 2 is even
## 3 is odd
## 4 is even
# Bucle while
# En este caso x debe ir cambiando dentro del loop 
# para cumplir una condición y así poder detenerse 
x = 4
i = 0
while (x >= 0):
    i = i + 1
    print('In iter {0}: x = {1}'.format(i, x))
    x = x - 1
## In iter 1: x = 4
## In iter 2: x = 3
## In iter 3: x = 2
## In iter 4: x = 1
## In iter 5: x = 0

3 Clases en Python

Una clase en Python es un tipod de dato personalizado por el usuario (antes vimos int, float, strings). Aquí se define una clase llamada Persona con atributos como nombre, DNI y edad, junto con métodos personalizados para representar el objeto y extraer iniciales del nombre.

class Persona:
  def __init__(self, nombre, dni, edad):
    self.nombre = nombre
    self.dni = dni
    self.edad = edad

  def iniciales(self):
    cadena = ''
    for c in self.nombre.title():
      if c >= 'A' and c <= 'Z':
        cadena =  cadena + c + '. '
    return cadena

  def __str__(self):
    cadena = 'Nombre: {0}\n'.format(self.nombre)
    cadena += 'D.N.I.: {0}\n'.format(self.dni)
    cadena += 'Edad: {0}\n'.format(self.edad)
    return cadena

juan = Persona('Juan Perez', 52123654, 15)
print(juan)
## Nombre: Juan Perez
## D.N.I.: 52123654
## Edad: 15
print('Nombre: {0} \nDNI: {1} \nEdad: {2}'.format(juan.nombre, juan.dni, juan.edad))
## Nombre: Juan Perez 
## DNI: 52123654 
## Edad: 15
print('Iniciales: {0}'.format(juan.iniciales()))
## Iniciales: J. P.
peter = Persona('pedro paz', 53356987, 20)
print('Iniciales: {0}'.format(peter.iniciales()))
## Iniciales: P. P.

4 NumPy: Matrices y computo vectorizado

NumPy es uno de los paquetes más importantes para el cálculo numérico en Python.

import numpy as np

my_arr = np.arange(10) # arange es la funcion range de Numpy
my_arr                 # Vector
## array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
matriz = np.array([[1.5, -0.1, 3], [0, -3, 6.5]])
matriz
## array([[ 1.5, -0.1,  3. ],
##        [ 0. , -3. ,  6.5]])
matriz.shape
## (2, 3)
matriz.dtype
## dtype('float64')
np.eye(3)
## array([[1., 0., 0.],
##        [0., 1., 0.],
##        [0., 0., 1.]])

4.1 Algunos comando útiles de NumPy:

array: Convierte datos de entrada (lista, tupla, matriz u otro tipo de secuencia) en un ndarray

arange: Como el comando range, pero devuelve un ndarray en lugar de una lista

ones: Produce una matriz de todos los 1s con la forma y el tipo de datos dados

zeros: Igual que el anterior pero genera 0s.

empty: Crea nuevas matrices asignando nueva memoria, pero no las rellenes con ningún valor

eye, identity: Crea una matriz identidad cuadrada N × N (1 en la diagonal y 0 en el resto)

4.2 Indexación y segmentación (slicing)

arr = np.arange(10)
arr
## array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
arr[5]
## np.int64(5)
arr[5:8]
## array([5, 6, 7])
arr[5:8] = 12
arr
## array([ 0,  1,  2,  3,  4, 12, 12, 12,  8,  9])

Una primera distinción importante con respecto a las listas integradas de Python es que las segmentaciones de un array son vistas del array original. Esto significa que los datos no se copian y cualquier modificación en la vista se reflejará en el array de origen.

arr_slice = arr[5:8]
arr_slice 
## array([12, 12, 12])
arr_slice[1] = 12345
arr
## array([    0,     1,     2,     3,     4,    12, 12345,    12,     8,
##            9])
arr_slice[:] = 64
arr
## array([ 0,  1,  2,  3,  4, 64, 64, 64,  8,  9])

Para evitar este comportamiento exite la posibilidad de generar una copia

arr = np.arange(10)
arr_copy = arr.copy()
 

arr_copy[1] = 12345
arr
## array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
arr2d
## array([[1, 2, 3],
##        [4, 5, 6],
##        [7, 8, 9]])
arr2d[2]
## array([7, 8, 9])

Se puede acceder a elementos individuales recursivamente arr2d[0][2]. Sin embargo, esto es demasiado trabajo, por lo que se puede pasar una lista de índices separados por comas para seleccionar elementos individuales.

arr2d[0][2]
## np.int64(3)
arr2d[0, 2]
## np.int64(3)

También se puede cortar a lo largo del eje 0 (el primer eje, es decir, las filas). Por lo tanto, un corte selecciona un rango de elementos a lo largo de un eje. Puede ser útil interpretar la expresión arr2d[:2] como “seleccionar las dos primeras filas de arr2d”.

arr2d[:2]
## array([[1, 2, 3],
##        [4, 5, 6]])

Alternativamente:

arr2d[:2, 1:]
## array([[2, 3],
##        [5, 6]])

4.3 Transposición de matrices e intercambio de ejes

arr = np.arange(15).reshape((3, 5))
arr
## array([[ 0,  1,  2,  3,  4],
##        [ 5,  6,  7,  8,  9],
##        [10, 11, 12, 13, 14]])
arr.T
## array([[ 0,  5, 10],
##        [ 1,  6, 11],
##        [ 2,  7, 12],
##        [ 3,  8, 13],
##        [ 4,  9, 14]])

4.4 Generación de numeros aleatorios

np.random.seed(1234)  # Fijar semilla
samples = np.random.standard_normal(size=(4, 3))
samples
## array([[ 0.47143516, -1.19097569,  1.43270697],
##        [-0.3126519 , -0.72058873,  0.88716294],
##        [ 0.85958841, -0.6365235 ,  0.01569637],
##        [-2.24268495,  1.15003572,  0.99194602]])

4.5 Funciones universales de matrices elemento por elemento

arr = np.arange(10)
arr
## array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
np.sqrt(arr)
## array([0.        , 1.        , 1.41421356, 1.73205081, 2.        ,
##        2.23606798, 2.44948974, 2.64575131, 2.82842712, 3.        ])
np.exp(arr)
## array([1.00000000e+00, 2.71828183e+00, 7.38905610e+00, 2.00855369e+01,
##        5.45981500e+01, 1.48413159e+02, 4.03428793e+02, 1.09663316e+03,
##        2.98095799e+03, 8.10308393e+03])
np.log(arr) # logaritmo natural (base e)
## array([      -inf, 0.        , 0.69314718, 1.09861229, 1.38629436,
##        1.60943791, 1.79175947, 1.94591015, 2.07944154, 2.19722458])

4.6 Métodos matemáticos y estadísticos

# Forma alternativa de generar numeros aleatorios
# Más moderno, controlado y recomendable para proyectos grandes o reproducibles
rng = np.random.default_rng(5678)  # Crear generador con semilla
arr = rng.standard_normal((3, 3))
arr
## array([[-0.31156563,  0.66906888,  1.17536046],
##        [-0.67848623,  0.51281091, -0.5091413 ],
##        [-0.68022347,  0.0724691 , -1.72629606]])
arr.mean()
## np.float64(-0.1640003711947179)
np.mean(arr)
## np.float64(-0.1640003711947179)
arr.sum()
## np.float64(-1.4760033407524609)
arr.mean(axis=1)
## array([ 0.51095457, -0.22493887, -0.77801681])
(arr > 0).sum() # cantidad de valores positivos
## np.int64(4)

4.7 Otras funciones

names = np.array(["Bob", "Will", "Joe", "Bob", "Will", "Joe", "Joe"])
np.unique(names)
## array(['Bob', 'Joe', 'Will'], dtype='<U4')

4.8 Algebra

x = np.array([[1., 2., 3.], [4., 5., 6.]])
y = np.array([[6., 23.], [-1, 7], [8, 9]])

x
## array([[1., 2., 3.],
##        [4., 5., 6.]])
y
## array([[ 6., 23.],
##        [-1.,  7.],
##        [ 8.,  9.]])
np.dot(x, y)
## array([[ 28.,  64.],
##        [ 67., 181.]])

5 Trabajo con bases de datos

En esta sección se utiliza el paquete pandas que contiene estructuras de datos y herramientas de manipulación de datos diseñadas para que la limpieza y el análisis de datos sean rápidos.

se leen archivos Excel, se realiza el tratamiento típico de una base de datos: merge, renombrar variables, generar nuevas, eliminar columnas, agrupar, filtrar y reshaping.

import pandas as pd

os.chdir(r.path + '/OneDrive - BCRA/Python')

df = pd.read_excel('datos_WB.xlsx', sheet_name='1')
df.describe()
##               year    gdp_pc2015  ...    imports          popu
## count    90.000000     90.000000  ...  90.000000  9.000000e+01
## mean   2017.000000  24964.154669  ...  25.482920  7.648676e+07
## std       4.344698  13881.889348  ...   8.272329  5.975849e+07
## min    2010.000000   8435.011433  ...  11.780574  1.718146e+07
## 25%    2013.000000  12895.438247  ...  15.734811  4.438039e+07
## 50%    2017.000000  22024.599515  ...  28.936204  6.154354e+07
## 75%    2021.000000  36866.008807  ...  31.813062  6.754635e+07
## max    2024.000000  47682.761535  ...  39.594989  2.119986e+08
## 
## [8 rows x 8 columns]
meta = pd.read_excel('datos_WB.xlsx', sheet_name='2')

# Merge entre bases
df = pd.merge(df, meta , on='ccode', how='left')

# Renombrar
df.rename(columns={'popu': 'poblacion'}, inplace=True)

# Generar variable
df['open'] = df['exports'] + df['imports']

# Eliminar columnas
df.drop(['gdp_pc2015','poblacion','exports','imports'], axis=1, inplace=True)

# Info general
df.info()
## <class 'pandas.core.frame.DataFrame'>
## RangeIndex: 90 entries, 0 to 89
## Data columns (total 8 columns):
##  #   Column      Non-Null Count  Dtype  
## ---  ------      --------------  -----  
##  0   year        90 non-null     int64  
##  1   cname       90 non-null     object 
##  2   ccode       90 non-null     object 
##  3   gdp_pc2021  90 non-null     float64
##  4   credit_ps   89 non-null     float64
##  5   inv         90 non-null     float64
##  6   region      90 non-null     object 
##  7   open        90 non-null     float64
## dtypes: float64(4), int64(1), object(3)
## memory usage: 5.8+ KB
# Filtrar años posteriores a 2022
df2 = df[df.year > 2022]

# Agrupar por región y año, promediando variables
df3 = df2.groupby(['region','year']).mean(numeric_only=True).round(1)
df3
##                                 gdp_pc2021  credit_ps   inv  open
## region                    year                                   
## Europe & Central Asia     2023     53082.2       98.7  21.1  67.1
##                           2024     53366.0       94.0  20.6  64.3
## Latin America & Caribbean 2023     25249.5       65.3  19.7  40.3
##                           2024     25459.2       64.8  18.8  42.5
df3.reset_index(inplace=True)
df3
##                       region  year  gdp_pc2021  credit_ps   inv  open
## 0      Europe & Central Asia  2023     53082.2       98.7  21.1  67.1
## 1      Europe & Central Asia  2024     53366.0       94.0  20.6  64.3
## 2  Latin America & Caribbean  2023     25249.5       65.3  19.7  40.3
## 3  Latin America & Caribbean  2024     25459.2       64.8  18.8  42.5
# Pivot (reshape): años como columnas
df3.pivot_table(index='region', columns='year', values='gdp_pc2021').reset_index().round(1)
## year                     region     2023     2024
## 0         Europe & Central Asia  53082.2  53366.0
## 1     Latin America & Caribbean  25249.5  25459.2

6 Gráficos

Este bloque muestra cómo generar un gráfico de líneas con pyplot de matplotlib, con etiquetas, colores, título y guardado de imagen a archivo.

import matplotlib.pyplot as plt

AR = df.loc[:, ('ccode', 'year', 'gdp_pc2021')]
AR = AR[AR.ccode == 'ARG']

plt.figure(figsize=(10, 4))
plt.plot(AR.year, AR.gdp_pc2021, color='red', linestyle='--', linewidth=2, marker='o', label='GDPpc')
plt.xlabel('')
plt.ylabel('GDP pc PPP (constant 2021 int. $)')
plt.title('Argentina')
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()

plt.savefig('arg_gdp.png')