---
title: "0.5.2 pandas 3.0: Series, DataFrame y Copy-on-Write"
description: "Series y DataFrame de pandas 3.0 con precios de SINT: índices temporales, Copy-on-Write, el tipo str con PyArrow y las expresiones pd.col()."
url: "https://pythonparatrading.com/curso-python-trading/nivel-0/pandas-3-series-y-dataframe/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-0/pandas-3-series-y-dataframe.png"
---

# 0.5.2 · pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()

> pandas es la tabla de trabajo del research en Python: precios con fechas, operaciones con columnas de texto, resultados alineados por índice. La versión 3.0 cambia tres cosas que rompen código antiguo —Copy-on-Write, el tipo `str` y la resolución de las fechas— y añade `pd.col()`. Esta lección las enseña con los datos del curso.

- Curso: [Nivel 0: Python desde cero y mentalidad de datos](https://pythonparatrading.com/curso-python-trading/nivel-0/)
- Módulo: 0.5 · Computación numérica y datos
- Duración: 45 min
- Requisitos: [0.5.1 · NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios](https://pythonparatrading.com/curso-python-trading/nivel-0/numpy-arrays-y-vectorizacion/)

## Al terminar esta lección

- Leer un CSV de precios en un `DataFrame` con `DatetimeIndex` y seleccionar sesiones, meses y rangos de fechas.
- Explicar por qué dos `Series` se alinean por su índice y cómo aparecen los `NaN` cuando los calendarios no coinciden.
- Modificar un `DataFrame` en pandas 3.0 sin asignación encadenada, con `.loc` en un solo paso.
- Detectar columnas de texto con el tipo `str` y escribir columnas derivadas con `pd.col()` en `assign` y `loc`.

## Series y DataFrame

La lección 0.5.1 trabajaba con arrays de NumPy: números sin nombre y sin fecha. Para research hace falta más: saber a
qué sesión corresponde cada cierre, qué columna es el volumen y qué operación es de qué activo. pandas añade eso sobre
NumPy con dos estructuras:

- **`Series`**: un array de una dimensión con un **índice** (las etiquetas de las filas) y un nombre. Los cierres de
  SINT son una `Series` indexada por fecha.
- **`DataFrame`**: varias columnas que comparten el mismo índice, cada una con su tipo. Las barras diarias de SINT
  (apertura, máximo, mínimo, cierre y volumen) son un `DataFrame`.

Las versiones del curso son las de `curso-code/uv.lock`: pandas 3.0.6, la serie 3.0 que se publicó en enero de 2026.
Todo lo que sigue funciona con ella, y las diferencias con pandas 2 se señalan donde aparecen.

```python
import numpy as np
import pandas as pd

precios = pd.read_csv("sint_diario.csv", parse_dates=["fecha"],
                      index_col="fecha")
print(pd.__version__, type(precios).__name__, precios.shape)
print(precios.dtypes)
precios.head(3)
```
```output
3.0.6 DataFrame (1303, 5)
apertura    float64
maximo      float64
minimo      float64
cierre      float64
volumen       int64
dtype: object
            apertura  maximo  minimo  cierre  volumen
fecha
2021-01-04    100.02  100.64   99.07   99.14  2322609
2021-01-05     99.32   99.79   98.63   99.25  1517954
2021-01-06     99.10   99.24   97.37   97.38  3245658
```

`parse_dates` convierte la columna de texto en fechas e `index_col` la usa como índice. Sin esas dos opciones, las
fechas serían texto y el índice, los números de fila 0, 1, 2…: todo lo que viene después dejaría de funcionar. Cada
columna tiene su tipo: los precios son `float64` y el volumen, `int64`, porque es un número entero de títulos.

Una columna de un `DataFrame` es una `Series`. Las operaciones vectorizadas de NumPy siguen funcionando, y pandas
añade métodos con nombre para las más comunes en finanzas, como `pct_change` (la rentabilidad simple de la lección
0.5.1):

```python
cierre = precios["cierre"]
print(type(cierre).__name__, cierre.name, cierre.dtype)
r = cierre.pct_change()
print(r.head(3))
print(f"Media diaria: {r.mean():.4%}  ·  sesiones: {r.count()}")
```
```output
Series cierre float64
fecha
2021-01-04         NaN
2021-01-05    0.001110
2021-01-06   -0.018841
Name: cierre, dtype: float64
Media diaria: 0.0342%  ·  sesiones: 1302
```

La primera rentabilidad es `NaN`: el primer día no tiene cierre anterior. pandas no la elimina y, a diferencia de
`np.mean`, `mean()` y `count()` ignoran los `NaN` por defecto, por eso salen 1.302 rentabilidades de 1.303 cierres.
Es cómodo y peligroso a la vez: un hueco en los datos no rompe la media, así que tampoco avisa.

### Alineación por índice

La diferencia de fondo entre pandas y NumPy es la **alineación**: cuando dos `Series` operan juntas, pandas empareja
los valores por su etiqueta, no por su posición. Con dos activos que cotizan en fechas distintas —un mercado cierra
por festivo, un proveedor no tiene un día—, el resultado tiene la unión de las fechas y `NaN` donde falta uno de los
dos. Un ejemplo con SINT y `RVAR`, el activo de renta variable de `cartera_sint.csv`, en dos tramos que solo se solapan
dos días:

```python
cartera = pd.read_csv("cartera_sint.csv", parse_dates=["fecha"],
                      index_col="fecha")
sint = cierre["2024-01-02":"2024-01-05"]
rvar = cartera["RVAR"]["2024-01-04":"2024-01-09"]
(sint / rvar).round(4)
```
```output
fecha
2024-01-02       NaN
2024-01-03       NaN
2024-01-04    0.7574
2024-01-05    0.7577
2024-01-08       NaN
2024-01-09       NaN
dtype: float64
```

Con arrays de NumPy, dividir dos vectores de cuatro elementos habría emparejado el 2 de enero de SINT con el 4 de
enero de `RVAR` sin dar ningún error: un ratio entre precios de días distintos. pandas lo impide: solo divide lo que
corresponde a la misma fecha y marca el resto como ausente. Qué hacer con esos ausentes (descartarlos, arrastrar el
último precio) es una decisión que la lección 0.5.3 trata con `merge`, `join` y `reindex`.

## Índices temporales

Un índice de fechas, `DatetimeIndex`, permite seleccionar por fechas escritas como texto. La selección parcial
—solo el año o el año y el mes— devuelve todas las sesiones de ese periodo, y los rangos con `loc` incluyen **los dos
extremos** (al contrario que los cortes por posición de Python):

```python
print(precios.index.dtype, precios.index.min().date(),
      precios.index.max().date())
print(len(precios.loc["2024"]), "sesiones en 2024")
print(precios.loc["2022-03", "cierre"].iloc[[0, -1]])
precios.loc["2022-06-01":"2022-06-07", ["cierre", "volumen"]]
```
```output
datetime64[us] 2021-01-04 2025-12-31
262 sesiones en 2024
fecha
2022-03-01    117.20
2022-03-31    111.28
Name: cierre, dtype: float64
            cierre  volumen
fecha
2022-06-01  108.39  1463683
2022-06-02  105.80  1832370
2022-06-03  104.44  1709652
2022-06-06  102.97  2301634
2022-06-07  102.23  1637924
```

El índice es `datetime64[us]`: fechas con resolución de **microsegundo**. Es un cambio de pandas 3.0: al leer fechas
de texto, la resolución por defecto pasa de nanosegundo a microsegundo (la de nanosegundo solo se usa si el texto lo
necesita). Para seleccionar o comparar fechas no cambia nada; donde muerde es al convertir fechas en números, algo
habitual al enviar marcas de tiempo a la API de un bróker, que suele esperarlas en milisegundos desde 1970:

```python
tres = precios.index[:3]
print(tres.astype("int64").tolist())
print(tres.as_unit("ns").astype("int64").tolist())
print(tres.as_unit("ms").astype("int64").tolist())
```
```output
[1609718400000000, 1609804800000000, 1609891200000000]
[1609718400000000000, 1609804800000000000, 1609891200000000000]
[1609718400000, 1609804800000, 1609891200000]
```

El mismo 4 de enero de 2021 se convierte en un número mil veces menor que en pandas 2. Un código antiguo que dividía
entre `10**6` para pasar de nanosegundos a milisegundos ahora produce fechas de 1970 sin dar ningún error. La regla:
antes de convertir a entero, fija la unidad con `as_unit`.

El índice sabe qué día de la semana es cada sesión, cuántas hay por año y si hay huecos. Un vistazo rápido al calendario
de los datos antes de calcular nada evita sorpresas:

```python
print(precios.index.year.value_counts().sort_index().to_dict())
print((precios.index.dayofweek >= 5).sum(), "sesiones en fin de semana")
precios.loc["2025-12-23":"2025-12-29", "cierre"]
```
```output
{2021: 260, 2022: 260, 2023: 260, 2024: 262, 2025: 261}
0 sesiones en fin de semana
fecha
2025-12-23    139.43
2025-12-24    139.36
2025-12-25    139.32
2025-12-26    139.23
2025-12-29    139.56
Name: cierre, dtype: float64
```

No hay fines de semana, pero el 25 de diciembre de 2025 tiene cierre. No es un error de lectura: SINT es un activo
ficticio que cotiza todos los días hábiles, **sin festivos**, como dice el README de los datos. Un mercado real cierra en
Navidad, y los calendarios de cada bolsa (lección 0.4.4) son la referencia para saber qué sesiones deberían existir.
Comprobar el calendario es el primer paso de cualquier limpieza de datos: un festivo con precio, o una sesión que falta,
desplaza todas las rentabilidades de alrededor.

<aside class="callout" data-kind="nota">

Estas fechas no tienen zona horaria (*naive*). Para datos diarios basta; para barras intradía de varios mercados, el
índice se localiza con `tz_localize("America/New_York")` y se convierte con `tz_convert("Europe/Madrid")`, con las
mismas reglas de `zoneinfo` de la lección 0.4.4.

</aside>

## Copy-on-Write: cada objeto es suyo

En pandas 2, `precios["cierre"]` podía devolver una **vista**: un objeto que compartía memoria con el `DataFrame`, de
modo que cambiarlo cambiaba también el original. Otras operaciones devolvían una **copia**. Cuál de las dos dependía de
detalles internos, y el aviso `SettingWithCopyWarning` intentaba, sin mucho éxito, avisar de los casos dudosos.

pandas 3.0 elimina la ambigüedad con **Copy-on-Write** (CoW): el resultado de cualquier indexado o método se comporta
como una copia. Por dentro no copia nada mientras nadie escriba (por eso es rápido); la copia real se hace en el momento
de la primera escritura:

```mermaid
flowchart LR
  A[precios] --> B[cierre]
  B -->|lectura| C[Memoria común]
  A -->|lectura| C
  B -->|escritura| D[Copia de cierre]
```

En la práctica, solo hay una forma de cambiar un objeto: escribir sobre ese objeto directamente.

```python
df = precios.copy()
col = df["cierre"]
col.iloc[0] = 0.0          # se escribe en la Series extraída
print(col.iloc[0], df["cierre"].iloc[0])
```
```output
0.0 99.14
```

`col` cambia y `df` no. En pandas 2 sin CoW, esta misma celda habría puesto un cero en el primer cierre de `df`: un
precio de cero que nadie escribió a propósito y que aparece después como una caída del 100 % en un backtest.

La otra cara es la **asignación encadenada**: dos indexados seguidos y una asignación al final. Es el error más frecuente
al construir una columna de señal, y en pandas 3.0 no funciona nunca:

```python
import warnings

df["senal"] = 0
with warnings.catch_warnings(record=True) as avisos:
    warnings.simplefilter("always")
    df["senal"][df["cierre"] > 120] = 1     # encadenada
print([a.category.__name__ for a in avisos])
print("Con asignación encadenada:", df["senal"].sum())

df.loc[df["cierre"] > 120, "senal"] = 1     # un solo paso
print("Con .loc:", df["senal"].sum())
```
```output
['ChainedAssignmentError']
Con asignación encadenada: 0
Con .loc: 523
```

`df["senal"]` devuelve un objeto intermedio que se comporta como una copia; la asignación se hace sobre él y se pierde.
pandas lo detecta y emite el aviso `ChainedAssignmentError` (en la celda se recoge con `warnings` para mostrar solo su
nombre; en un notebook sale el mensaje completo). La versión con `.loc` indica filas y columna en una sola operación
sobre `df`, y es la única que cambia el `DataFrame`: 523 sesiones con cierre por encima de 120.

| Patrón | pandas 2 | pandas 3.0 |
|---|---|---|
| `df["c"][mascara] = v` | Cambiaba `df` (a veces con aviso) | No cambia nada; `ChainedAssignmentError` |
| `s = df["c"]; s.iloc[0] = v` | Podía cambiar `df` | Solo cambia `s` |
| `df.loc[mascara, "c"] = v` | Cambia `df` | Cambia `df` |
| `sub = df[mascara].copy()` | `.copy()` para callar el aviso | `.copy()` innecesario |

<aside class="callout" data-kind="aviso">

Al migrar código antiguo, busca asignaciones con dos pares de corchetes seguidos de `=`. Si el resultado de un
backtest cambia al pasar a pandas 3.0, casi siempre es una columna de señal o de posición que antes se rellenaba por
asignación encadenada y ahora se queda en su valor inicial.

</aside>

## El tipo str y PyArrow

Hasta pandas 2, una columna de texto era de tipo `object`: un array de punteros a cadenas de Python, cada una en su
propio sitio de la memoria, igual que una lista. pandas 3.0 infiere por defecto un tipo de texto propio, **`str`**,
guardado con PyArrow cuando está instalado (en el proyecto del curso lo está, porque lo usa Parquet). El libro de
operaciones ficticias tiene dos columnas de texto:

```python
ops = pd.read_csv("operaciones_sint.csv",
                  parse_dates=["fecha_entrada", "fecha_salida"])
print(ops.dtypes)
print(ops["lado"].dtype.storage, ops["lado"].dtype.na_value)
ops.iloc[:3, :6]
```
```output
id                         int64
fecha_entrada     datetime64[us]
fecha_salida      datetime64[us]
simbolo                      str
lado                         str
cantidad                   int64
precio_entrada           float64
precio_salida            float64
comision                 float64
dtype: object
pyarrow nan
   id fecha_entrada fecha_salida simbolo   lado  cantidad
0   1    2021-03-04   2021-04-26     ORO  corto       200
1   2    2021-09-02   2021-10-12    SINT  largo       180
2   3    2021-10-27   2021-12-08    SINT  corto        10
```

`simbolo` y `lado` son `str`, con almacenamiento `pyarrow` y `NaN` como valor ausente (el mismo que en las columnas
numéricas). Las fechas de entrada y salida, como el índice de precios, son `datetime64[us]`.

El cambio es intencionado y rompe un patrón muy extendido: buscar las columnas de texto comparando su tipo con
`object`.

```python
lado = ops["lado"]
print(lado.dtype == object)
print(lado.dtype == "str")
print(pd.api.types.is_string_dtype(lado))
print(ops.select_dtypes(include="str").columns.tolist())
```
```output
False
True
True
['simbolo', 'lado']
```

`dtype == object` devuelve `False` sin dar error: el código que limpiaba las columnas de texto así simplemente deja de
encontrarlas. Las dos formas robustas son `pd.api.types.is_string_dtype`, que acepta el tipo antiguo y el nuevo, y
`select_dtypes(include="str")`. `select_dtypes(include="object")` todavía incluye las de texto por compatibilidad, pero
con un aviso de que dejará de hacerlo.

El tipo `str` es además más estricto: solo admite cadenas o ausentes. Un número en la columna `lado` era posible con
`object` y era un error latente; ahora se rechaza al escribirlo:

```python raises
ops.loc[0, "lado"] = 1
```
```output
TypeError: Invalid value '1' for dtype 'str'. Value should be a string or missing value, got 'int' instead.
```

Las operaciones de texto siguen en el accesor `.str` (`ops["simbolo"].str.upper()`, `.str.startswith("l")`) y las
comparaciones (`ops["lado"] == "largo"`) funcionan igual. Con PyArrow, además, la columna ocupa menos memoria y esas
operaciones suelen ser más rápidas que con `object`: en `ops`, `lado` ocupa 520 bytes como `str` y 2.160 como
`object` (`memory_usage(deep=True)`). En tablas de millones de órdenes, la diferencia se nota.

## Expresiones con pd.col()

Para crear columnas derivadas, `assign` aceptaba hasta ahora valores o funciones que reciben el propio `DataFrame`
(`lambda d: d["cierre"] / ...`). pandas 3.0 añade **`pd.col("nombre")`**: una referencia a una columna que se puede
combinar con operadores y con los métodos de `Series`, y que pandas evalúa sobre el `DataFrame` en el que se usa. Es la
misma idea que las expresiones de Polars (lección 0.5.4):

```python
barras = precios.assign(
    r=pd.col("cierre").pct_change(),
    rango=(pd.col("maximo") - pd.col("minimo")) / pd.col("cierre"),
)
print(pd.col("cierre").pct_change())
barras[["cierre", "r", "rango"]].head(3).round(4)
```
```output
col('cierre').pct_change()
            cierre       r   rango
fecha
2021-01-04   99.14     NaN  0.0158
2021-01-05   99.25  0.0011  0.0117
2021-01-06   97.38 -0.0188  0.0192
```

Impreso, `pd.col("cierre").pct_change()` no es un número ni una columna: es una **expresión** pendiente de evaluar.
Por eso se puede guardar en una variable y reutilizar. En el libro de operaciones, el signo de cada operación (1 para
`largo`, −1 para `corto`) sirve para calcular su resultado neto de comisiones:

$$
\text{pnl} = \text{signo} \cdot (P_{\text{salida}} - P_{\text{entrada}}) \cdot \text{cantidad} - \text{comisiones}
$$

```python
signo = pd.col("lado").map({"largo": 1, "corto": -1})
ops = ops.assign(
    pnl=signo * (pd.col("precio_salida") - pd.col("precio_entrada"))
    * pd.col("cantidad") - pd.col("comision"),
)
print(f"Resultado total: {ops['pnl'].sum():,.2f}")
ops.loc[pd.col("pnl") < -1000, ["id", "simbolo", "lado", "pnl"]]
```
```output
Resultado total: 1,149.19
    id simbolo   lado      pnl
0    1     ORO  corto -3123.56
7    8    SINT  largo -1288.04
14  15    SINT  largo -1058.60
```

`pd.col` funciona en cualquier sitio que acepte una función del `DataFrame`: `assign`, `loc` y la selección con
corchetes. En `loc`, la expresión `pd.col("pnl") < -1000` devuelve la máscara de las operaciones con una pérdida mayor
de 1.000 en la moneda del activo: tres de las cuarenta, una en `ORO` y dos en SINT. El libro en conjunto gana 1.149,19
netos de comisiones (Python formatea los miles con coma: `1,149.19`). La ventaja frente a `df["pnl"] < -1000` aparece al encadenar métodos: la expresión
se refiere al `DataFrame` del paso en que se evalúa, no a una variable que puede haber quedado desfasada.

## Ejercicios

1. Este código intenta marcar con un 1 en `alerta` las sesiones de SINT con un volumen mayor que el doble de la mediana,
   pero la columna se queda a cero. Explica por qué y corrígelo.

   ```python skip
   v = precios.copy()
   v["alerta"] = 0
   v[v["volumen"] > 2 * v["volumen"].median()]["alerta"] = 1
   ```

**Solución.**

```python
v = precios.copy()
v["alerta"] = 0
v.loc[v["volumen"] > 2 * v["volumen"].median(), "alerta"] = 1
print(v["alerta"].sum(), "sesiones con volumen anómalo")
```
```output
51 sesiones con volumen anómalo
```

`v[mascara]` devuelve un `DataFrame` nuevo que se comporta como una copia; `["alerta"] = 1` lo modifica a él y se
pierde. Con `.loc[mascara, "alerta"]`, filas y columna se indican en una sola operación sobre `v`, y se marcan 51
sesiones.

2. Con `pd.col()`, añade a `precios` una columna `hueco` con el *gap* de apertura (apertura de hoy entre cierre de ayer,
   menos uno) y selecciona con `loc` las sesiones de 2022 en que abrió más de un 1 % por debajo del cierre anterior.

**Solución.**

```python
gaps = precios.assign(
    hueco=pd.col("apertura") / pd.col("cierre").shift(1) - 1,
)
bajistas = gaps.loc["2022"].loc[pd.col("hueco") < -0.01]
print(len(bajistas), "huecos bajistas de más del 1 %")
bajistas[["apertura", "cierre", "hueco"]].round(4)
```
```output
3 huecos bajistas de más del 1 %
            apertura  cierre   hueco
fecha
2022-08-22    109.40  108.59 -0.0127
2022-08-29    111.41  115.26 -0.0108
2022-09-02    113.75  117.11 -0.0109
```

`shift(1)` desplaza los cierres una sesión hacia delante, así que cada fila compara su apertura con el cierre anterior:
información disponible en el momento de la apertura, sin anticipación. Salen tres sesiones, todas entre finales de
agosto y principios de septiembre. El primer `loc` selecciona el año y el segundo
filtra con la expresión, que se evalúa sobre el `DataFrame` ya recortado.

3. Escribe una función `columnas_texto(df)` que devuelva los nombres de las columnas de texto de un `DataFrame` y que
   funcione igual con columnas `str` y con columnas `object` de pandas 2. Pruébala con `ops` y con
   `ops.astype({"lado": object})`.

**Solución.**

```python
def columnas_texto(df: pd.DataFrame) -> list[str]:
    return [c for c in df.columns
            if pd.api.types.is_string_dtype(df[c])]

print(columnas_texto(ops))
print(columnas_texto(ops.astype({"lado": object})))
```
```output
['simbolo', 'lado']
['simbolo', 'lado']
```

`is_string_dtype` reconoce el tipo `str` y también una columna `object` cuyos valores son cadenas, así que la función
da el mismo resultado con los dos tipos. Comparar con `object` solo encontraría la segunda.

## Autoevaluación

1. En pandas 3.0, `df[df["cierre"] > 120]["senal"] = 1`. ¿Qué le pasa a `df`?

   - A. Se pone 1 en `senal` en los días con cierre mayor que 120
   - B. Nada: la asignación se hace sobre un objeto intermedio que se comporta como una copia, y pandas avisa con `ChainedAssignmentError`
   - C. Lanza una excepción y el programa se para
   - D. Se pone 1 en todas las filas

   *Respuesta B: Nada: la asignación se hace sobre un objeto intermedio que se comporta como una copia, y pandas avisa con `ChainedAssignmentError`.* Con Copy-on-Write, el resultado de cualquier indexado se comporta como una copia. La asignación encadenada nunca llega al original; la forma correcta es `df.loc[df["cierre"] > 120, "senal"] = 1`.

2. Una columna `lado` con «largo» y «corto» se lee con pandas 3.0 y PyArrow instalado. ¿Qué devuelve `df["lado"].dtype == object`?

   - A. `True`, como en pandas 2
   - B. `False`: la columna es del tipo `str`
   - C. Un error de tipo
   - D. Depende del contenido de la primera fila

   *Respuesta B: `False`: la columna es del tipo `str`.* pandas 3.0 infiere el tipo `str` para el texto. Las comprobaciones de `object` dejan de encontrar esas columnas; lo robusto es `pd.api.types.is_string_dtype`.

3. Se suman los cierres de dos activos con calendarios distintos: `a + b`. ¿Qué pasa con las fechas que solo están en uno de los dos?

   - A. Se suman por posición, sin mirar las fechas
   - B. Se descartan en silencio
   - C. Aparecen en el resultado con `NaN`
   - D. Se rellenan con el último precio conocido

   *Respuesta C: Aparecen en el resultado con `NaN`.* pandas alinea por índice: el resultado tiene la unión de las fechas, y donde falta uno de los dos operandos el valor es `NaN`. Rellenar o descartar es una decisión explícita.

4. En pandas 3.0, `pd.read_csv(..., parse_dates=["fecha"])` crea fechas `datetime64[us]`. ¿Qué devuelve `index.astype("int64")`?

   - A. Nanosegundos desde 1970, como en pandas 2
   - B. Microsegundos desde 1970: números 1.000 veces menores que en pandas 2
   - C. Segundos desde 1970
   - D. Días desde 1970

   *Respuesta B: Microsegundos desde 1970: números 1.000 veces menores que en pandas 2.* La resolución por defecto al leer texto pasa a ser el microsegundo. Para obtener otra unidad hay que pedirla con `as_unit("ns")` o `as_unit("ms")` antes de convertir a entero.

## Ideas clave

- Una `Series` es una columna con índice; un `DataFrame`, varias columnas que comparten índice. Toda operación entre ellas se alinea por índice, no por posición.
- Con un `DatetimeIndex`, `loc["2022-03"]` selecciona un mes y `loc["2022-06-01":"2022-06-07"]` un rango con los dos extremos incluidos.
- Copy-on-Write: cualquier indexado se comporta como una copia. Para cambiar un `DataFrame`, se asigna sobre él en un solo paso con `.loc`; la asignación encadenada no hace nada.
- El texto es del tipo `str` (PyArrow por debajo): las comprobaciones `dtype == object` fallan en silencio y la columna solo admite cadenas o `NaN`.
- `pd.col("cierre")` construye una expresión que `assign` y `loc` evalúan sobre el propio `DataFrame`, sin `lambda`.

## Recursos

### Documentación

- [pandas 3.0.0: novedades (What's new)](https://pandas.pydata.org/docs/whatsnew/v3.0.0.html) · pandas.pydata.org
- [pandas: Copy-on-Write (guía de usuario)](https://pandas.pydata.org/docs/user_guide/copy_on_write.html) · pandas.pydata.org
- [pandas: migración al tipo de texto de pandas 3.0](https://pandas.pydata.org/docs/user_guide/migration-3-strings.html) · pandas.pydata.org
- [pandas: series temporales (guía de usuario)](https://pandas.pydata.org/docs/user_guide/timeseries.html) · pandas.pydata.org

### Libros

- [Wes McKinney, Python for Data Analysis, 3.ª ed. (O'Reilly, 2022; edición abierta)](https://wesmckinney.com/book/) · wesmckinney.com

### Datos

- [Datos del nivel 0: operaciones ficticias (CSV)](https://pythonparatrading.com/curso/datos/nivel-0/operaciones_sint.csv) · Descarga · operaciones_sint.csv

Lección anterior: [0.5.1 · NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios](https://pythonparatrading.com/curso-python-trading/nivel-0/numpy-arrays-y-vectorizacion/)

Lección siguiente: [0.5.3 · Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles](https://pythonparatrading.com/curso-python-trading/nivel-0/pandas-groupby-merge-resample-y-rolling/)
