Introducción
La introducción de Copy-on-Write (CoW) es un cambio importante que tendrá cierto impacto en el código pandas existente. Investigaremos cómo podemos adaptar nuestro código para evitar errores cuando CoW esté habilitado de forma predeterminada. Actualmente, esto está previsto para el lanzamiento de pandas 3.0, que está previsto para abril de 2024. la primera publicación En esta serie se explica el comportamiento de Copiar en escritura mientras la segunda publicación Se sumergió en optimizaciones de rendimiento relacionadas con Copiar en escritura.
Estamos planeando agregar un modo de advertencia que advertirá sobre todas las operaciones que cambiarán el comportamiento con CoW. La advertencia será muy ruidosa para los usuarios y, por lo tanto, debe tratarse con cierto cuidado. Esta publicación explica casos comunes y cómo puede adaptar su código para evitar cambios en el comportamiento.
Asignación encadenada
La asignación encadenada es una técnica en la que un objeto se actualiza mediante 2 operaciones posteriores.
import pandas as pddf = pd.DataFrame({"x": [1, 2, 3]})
df["x"][df["x"] > 1] = 100
La primera operación selecciona la columna. "x" mientras que la segunda operación restringe el número de filas. Hay muchas combinaciones diferentes de estas operaciones (por ejemplo, combinadas con loc o iloc). Ninguna de estas combinaciones funcionará bajo CoW. En cambio, lanzarán una advertencia. ChainedAssignmentError eliminar estos patrones en lugar de no hacer nada en silencio.
Generalmente, puedes usar loc en cambio:
df.loc[df["x"] > 1, "x"] = 100
La primera dimensión de loc siempre corresponde a la row-indexer. Esto significa que puede seleccionar un subconjunto de filas. La segunda dimensión corresponde a la column-indexerque le permite seleccionar un subconjunto de columnas.
Generalmente es más rápido usando loc cuando desee establecer valores en un subconjunto de filas, esto limpiará su código y proporcionará una mejora del rendimiento.
Este es el caso obvio en el que CoW tendrá un impacto. También afectará las operaciones in situ encadenadas:
df["x"].replace(1, 100)
El patrón es el mismo que el anterior. La selección de columnas es la primera operación. El replace El método intenta operar en el objeto temporal, que no podrá actualizar el objeto inicial. También puede eliminar estos patrones con bastante facilidad especificando las columnas en las que desea operar.
df = df.replace({"x": 1}, {"x": 100})
Patrones a evitar
mi publicación anterior explica cómo funciona el mecanismo CoW y cómo los DataFrames comparten los datos subyacentes. Se realizará una copia defensiva si dos objetos comparten los mismos datos mientras modifica un objeto in situ.
df2 = df.reset_index()
df2.iloc[0, 0] = 100
El reset_index La operación creará una vista de los datos subyacentes. El resultado se asigna a una nueva variable. df2, esto significa que dos objetos comparten los mismos datos. Esto es cierto hasta df es basura recolectada. El setitem La operación activará así una copia. Esto es completamente innecesario si no necesitas el objeto inicial. df ya no. Simplemente reasignar a la misma variable invalidará la referencia que tiene el objeto.
df = df.reset_index()
df.iloc[0, 0] = 100
En resumen, la creación de múltiples referencias con el mismo método mantiene vivas las referencias innecesarias.
Las referencias temporales que se crean al encadenar diferentes métodos están bien.
df = df.reset_index().drop(...)
Esto sólo mantendrá viva una referencia.
Accediendo a la matriz NumPy subyacente
Actualmente, pandas nos da acceso a la matriz NumPy subyacente a través de to_numpy o .values. La matriz devuelta es una copia, si su DataFrame consta de diferentes tipos, por ejemplo:
df = pd.DataFrame({"a": [1, 2], "b": [1.5, 2.5]})
df.to_numpy()[[1. 1.5]
[2. 2.5]]
El DataFrame está respaldado por dos matrices que deben combinarse en una. Esto desencadena la copia.
El otro caso es un DataFrame que solo está respaldado por una única matriz NumPy, por ejemplo:
df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
df.to_numpy()[[1 3]
[2 4]]
Podemos acceder directamente a la matriz y obtener una vista en lugar de una copia. Esto es mucho más rápido que copiar todos los datos. Ahora podemos operar en la matriz NumPy y potencialmente modificarla en el lugar, lo que también actualizará el DataFrame y potencialmente todos los demás DataFrames que comparten datos. Esto se vuelve mucho más complicado con Copiar en escritura, ya que eliminamos muchas copias defensivas. Muchos más DataFrames ahora compartirán memoria entre sí.
to_numpy y .values devolverá una matriz de solo lectura debido a esto. Esto significa que no se puede escribir en la matriz resultante.
df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
arr = df.to_numpy()arr[0, 0] = 1
Esto desencadenará un ValueError:
ValueError: assignment destination is read-only
Puedes evitar esto de dos maneras diferentes:
- Active una copia manualmente si desea evitar actualizar los DataFrames que comparten memoria con su matriz.
- Haga que la matriz se pueda escribir. Esta es una solución más eficaz pero elude las reglas de copia en escritura, por lo que debe usarse con precaución.
arr.flags.writeable = True
Hay casos en los que esto no es posible. Una ocurrencia común es, si accede a una sola columna respaldada por PyArrow:
ser = pd.Series([1, 2], dtype="int64[pyarrow]")
arr = ser.to_numpy()
arr.flags.writeable = True
Esto devuelve un ValueError:
ValueError: cannot set WRITEABLE flag to True of this array
Las matrices de flechas son inmutables, por lo que no es posible hacer que se pueda escribir en la matriz NumPy. La conversión de Arrow a NumPy es de copia cero en este caso.
Conclusión
Hemos analizado los cambios más invasivos relacionados con la copia en escritura. Estos cambios se convertirán en el comportamiento predeterminado en pandas 3.0. También hemos investigado cómo podemos adaptar nuestro código para evitar romperlo cuando Copiar en escritura está habilitado. El proceso de actualización debería ser bastante sencillo si puedes evitar estos patrones.