Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Introducción: fundamentos de Python

¿Cuánto Python necesitamos saber?

Python básico

La mayoría de los estudiantes comienzan este módulo después de una introducción a Python o ya están familiarizados con los fundamentos de Python. Por ejemplo, deberías sentirte cómodo/a con lo esencial de Python, como asignar variables,

x = 5

sentencias if,

if x < 5:
    print("pequeño")
else:
    print("grande")
grande

y bucles.

for i in range(x):
    print(i)
0
1
2
3
4

Tu análisis de datos probablemente estará lleno de sentencias como estas, aunque los tipos de operaciones en las que nos enfocaremos en este módulo tienen una forma más parecida a

import libreria_compilada

libreria_compilada.operacion_computacionalmente_costosa(array_grande)

El truco está en que el código del lado de Python sea lo suficientemente expresivo y el código compilado lo suficientemente general para que no necesites una nueva libreria_compilada para cada cosa que quieras hacer. Las librerías presentadas en este módulo están diseñadas con interfaces que te permiten expresar lo que quieres hacer en Python y ejecutarlo en código compilado.

Interfaces tipo diccionario y tipo array

Los dos tipos de datos más importantes para estas interfaces son los diccionarios

un_diccionario = {"palabra": 1, "otra palabra": 2, "alguna otra palabra": 3}
un_diccionario["alguna otra palabra"]
3
for clave in un_diccionario:
    print(clave, un_diccionario[clave])
palabra 1
otra palabra 2
alguna otra palabra 3

y los arrays

import numpy as np

un_array = np.array([0.0, 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9])
un_array[4]
np.float64(4.4)
for x in un_array:
    print(x)
0.0
1.1
2.2
3.3
4.4
5.5
6.6
7.7
8.8
9.9

Aunque estos tipos de datos son importantes, lo que es más importante son las interfaces de estos tipos. Ambos representan funciones de claves a valores:

  • los diccionarios (usualmente) mapean de cadenas a objetos de Python,

  • los arrays (siempre) mapean de enteros no negativos a valores numéricos.

La mayoría de los tipos que veremos en este módulo también mapean cadenas o enteros a datos, y usan la misma sintaxis que los diccionarios y los arrays. Si estás familiarizado/a con las interfaces de diccionarios y arrays, generalmente no tendrás que consultar la documentación sobre tipos parecidos a diccionarios o arrays, a menos que estés tratando de hacer algo especial.

Repaso de la interfaz de diccionario

Para los diccionarios, las cosas que pueden ir entre corchetes (su “dominio”, como una función) son sus claves (keys).

un_diccionario = {"palabra": 1, "otra palabra": 2, "alguna otra palabra": 3}
un_diccionario.keys()
dict_keys(['palabra', 'otra palabra', 'alguna otra palabra'])

Nada más que estas claves puede usarse entre corchetes

un_diccionario["algo que inventé"]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[11], line 1
----> 1 un_diccionario["algo que inventé"]

KeyError: 'algo que inventé'

a menos que haya sido agregado al diccionario.

un_diccionario["algo que inventé"] = 123
un_diccionario["algo que inventé"]
123

Los elementos que pueden salir de un diccionario (su “rango”, como una función) son sus valores (values).

un_diccionario.values()
dict_values([1, 2, 3, 123])

Puedes obtener las claves y los valores como tuplas de 2 elementos pidiendo sus elementos (items).

un_diccionario.items()
dict_items([('palabra', 1), ('otra palabra', 2), ('alguna otra palabra', 3), ('algo que inventé', 123)])
for clave, valor in un_diccionario.items():
    print(clave, valor)
palabra 1
otra palabra 2
alguna otra palabra 3
algo que inventé 123

Repaso de la interfaz de array

Para los arrays, las cosas que pueden ir entre corchetes (su “dominio”, como una función) son enteros desde cero hasta, pero sin incluir, su longitud.

import numpy as np

un_array = np.array([0.0, 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9])
un_array[0]  # está bien
np.float64(0.0)
un_array[1]  # está bien
np.float64(1.1)
un_array[9]  # está bien
np.float64(9.9)
un_array[10]  # no está bien
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[20], line 1
----> 1 un_array[10]  # no está bien

IndexError: index 10 is out of bounds for axis 0 with size 10

Puedes obtener la longitud de un array (y el número de claves en un diccionario) con len:

len(un_array)
10

Es importante recordar que el índice 0 corresponde al primer elemento del array, 1 al segundo, y así sucesivamente, por lo que len(un_array) no es un índice válido.

Se permiten índices negativos, pero cuentan desde el final de la lista hacia el principio. Por ejemplo,

un_array[-1]
np.float64(9.9)

devuelve el último elemento. Pregunta rápida: ¿qué valor negativo devuelve el primer elemento, equivalente a 0?

Los arrays también se pueden rebanar colocando dos puntos (:) entre el índice inicial y el índice final.

un_array[2:7]
array([2.2, 3.3, 4.4, 5.5, 6.6])

Pregunta rápida: ¿por qué 7.7 no está incluido en el resultado?

Lo anterior es común a todas las secuencias de Python. Sin embargo, los arrays pueden ser multidimensionales y esto permite más tipos de rebanado.

array3d = np.arange(2 * 3 * 5).reshape(2, 3, 5)

Eso da un array de 2 × 3 × 5 que contiene los números del 0 al 29, dispuestos así:

array3d

Separar dos rebanadas dentro de los corchetes con una coma

array3d[:, 1:, 1:]
array([[[ 6, 7, 8, 9], [11, 12, 13, 14]], [[21, 22, 23, 24], [26, 27, 28, 29]]])

selecciona lo siguiente:

array3d-resaltado1

Pregunta rápida: ¿cómo seleccionarías lo siguiente?

array3d-resaltado2

Filtrado con booleanos y enteros: “cortes”

Además de enteros y rebanadas, se pueden incluir arrays dentro de los corchetes. NumPy llama a esto indexación avanzada (advanced indexing), para distinguirla del rebanado básico con : que acabamos de ver. Vale la pena recordar la diferencia: una rebanada básica te da una vista del array original, así que asignarle valores modifica el original, mientras que la indexación avanzada siempre te da una copia.

Un array de booleanos con la misma longitud que el array rebanado selecciona todos los elementos que coinciden con True.

un_array = np.array([0.0, 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9])
array_booleano = np.array(
    [True, True, True, True, True, False, True, False, True, False]
)

un_array[array_booleano]
array([0. , 1.1, 2.2, 3.3, 4.4, 6.6, 8.8])

Un array de enteros selecciona elementos por índice.

un_array = np.array([0.0, 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9])
array_de_enteros = np.array([0, 1, 2, 3, 4, 6, 8])

un_array[array_de_enteros]
array([0. , 1.1, 2.2, 3.3, 4.4, 6.6, 8.8])

Indexar con un array de enteros es más general que indexar con un array de booleanos, porque un array de enteros también puede cambiar el orden de los datos y repetir elementos.

un_array[np.array([4, 2, 2, 2, 9, 8, 3])]
array([4.4, 2.2, 2.2, 2.2, 9.9, 8.8, 3.3])

Ambos aparecen en contextos naturales. Los arrays booleanos a menudo provienen de realizar un cálculo sobre todos los elementos de un array que devuelve valores booleanos.

elementos_pares = un_array * 10 % 2 == 0

un_array[elementos_pares]
array([0. , 2.2, 4.4, 6.6, 8.8])

Así es como vamos a calcular y aplicar cortes: expresiones como

corte_muon_bueno = (muones.pt > 10) & (abs(muones.eta) < 2.4)

muones_buenos = muones[corte_muon_bueno]

Operadores lógicos: &, |, ~, y paréntesis

Si vienes de C++, podrías esperar que “y”, “o”, “no” sean &&, ||, !.

Si vienes de Python no orientado a arrays, podrías esperar que sean and, or, not.

En las expresiones con arrays (¡desafortunadamente!), tenemos que usar los operadores bit a bit de Python, &, |, ~, y asegurarnos de que las comparaciones estén rodeadas de paréntesis. Los operadores and, or, not de Python no se aplican a lo largo de los arrays, y los operadores bit a bit tienen una precedencia sorprendente.

x = 0

x > -10 & x < 10  # ¡probablemente no es lo que esperas!
False
(x > -10) & (x < 10)
True
paréntesis con operadores bit a bit

¿Qué es el procesamiento “orientado a arrays” o “columnar”?

Expresiones como

elementos_pares = un_array * 10 % 2 == 0

realizan las operaciones *, % y == sobre cada elemento de un_array y devuelven arrays. Sin NumPy, lo anterior tendría que escribirse como

elementos_pares = []

for x in un_array:
    elementos_pares.append(x * 10 % 2 == 0)

Esto es más engorroso cuando deseas aplicar una fórmula matemática a cada elemento de una colección, pero también es considerablemente más lento. Cada paso en un bucle for en Python realiza verificaciones que son innecesarias para valores numéricos de tipo uniforme, verificaciones que se harían en tiempo de compilación en una librería compilada. NumPy es una librería compilada; sus operadores *, % y ==, así como muchas otras funciones, se ejecutan en bucles rápidos y compilados.

Así es como obtenemos expresividad y velocidad. Los lenguajes con operadores que se aplican a un array a la vez, en lugar de a un valor escalar a la vez, se llaman “orientados a arrays” o “columnares” (refiriéndose, por ejemplo, a las columnas de un DataFrame de Pandas).

Bastantes lenguajes interactivos de análisis de datos están orientados a arrays, derivados del APL original. “Orientado a arrays” es un paradigma de programación en el mismo sentido que “funcional” u “orientado a objetos”.

línea de tiempo de APL

¿Qué es un “ecosistema de software”?

Algunos entornos de programación, como Mathematica, Emacs y ROOT, intentan proporcionarte todo lo que necesitas en un solo paquete. Solo hay una cosa que instalar y los componentes dentro del framework deberían funcionar bien juntos porque se desarrollaron en conjunto. Sin embargo, puede ser difícil usar el framework con otros paquetes de software no relacionados.

Los ecosistemas, como UNIX, la App Store de iOS y Python, consisten en muchos paquetes de software pequeños que hacen una sola cosa y saben cómo comunicarse con otros paquetes a través de convenciones y protocolos. Por lo general, hay un mecanismo de instalación centralizado, y es responsabilidad del usuario (la tuya) reunir lo que necesitas. Sin embargo, el conjunto de posibilidades es abierto y crece a medida que surgen nuevas necesidades.

En el Python convencional, esto significa que

  • NumPy solo maneja arrays,

  • Pandas solo maneja tablas,

  • Matplotlib solo genera gráficos,

  • Jupyter solo proporciona una interfaz de notebook,

  • Scikit-Learn solo hace aprendizaje automático,

  • h5py solo interactúa con archivos HDF5,

  • etc.

Los paquetes de Python para física de altas energías se están desarrollando con un modelo similar:

  • Uproot solo lee y escribe archivos ROOT,

  • Awkward Array solo maneja arrays de tipos irregulares,

  • hist solo maneja histogramas,

  • iminuit solo optimiza,

  • zfit solo ajusta,

  • Particle solo proporciona datos al estilo PDG,

  • etc.

Para facilitar su localización, están catalogados bajo un nombre común en scikit-hep.org.

logos de Scikit-HEP