iloc - python pandas columns
¿Cómo lidiar con SettingWithCopyWarning en Pandas? (8)
Fondo
Acabo de actualizar mis Pandas de 0.11 a 0.13.0rc1. Ahora, la aplicación está apareciendo muchas nuevas advertencias. A uno de ellos le gusta esto:
E:/FinReporter/FM_EXT.py:449: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
quote_df[''TVol''] = quote_df[''TVol'']/TVOL_SCALE
Quiero saber qué significa exactamente? ¿Necesito cambiar algo?
¿Cómo debo suspender la advertencia si insisto en usar quote_df[''TVol''] = quote_df[''TVol'']/TVOL_SCALE
?
La función que da errores.
def _decode_stock_quote(list_of_150_stk_str):
"""decode the webpage and return dataframe"""
from cStringIO import StringIO
str_of_all = "".join(list_of_150_stk_str)
quote_df = pd.read_csv(StringIO(str_of_all), sep='','', names=list(''ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg'')) #dtype={''A'': object, ''B'': object, ''C'': np.float64}
quote_df.rename(columns={''A'':''STK'', ''B'':''TOpen'', ''C'':''TPCLOSE'', ''D'':''TPrice'', ''E'':''THigh'', ''F'':''TLow'', ''I'':''TVol'', ''J'':''TAmt'', ''e'':''TDate'', ''f'':''TTime''}, inplace=True)
quote_df = quote_df.ix[:,[0,3,2,1,4,5,8,9,30,31]]
quote_df[''TClose''] = quote_df[''TPrice'']
quote_df[''RT''] = 100 * (quote_df[''TPrice'']/quote_df[''TPCLOSE''] - 1)
quote_df[''TVol''] = quote_df[''TVol'']/TVOL_SCALE
quote_df[''TAmt''] = quote_df[''TAmt'']/TAMT_SCALE
quote_df[''STK_ID''] = quote_df[''STK''].str.slice(13,19)
quote_df[''STK_Name''] = quote_df[''STK''].str.slice(21,30)#.decode(''gb2312'')
quote_df[''TDate''] = quote_df.TDate.map(lambda x: x[0:4]+x[5:7]+x[8:10])
return quote_df
Mas mensajes de error
E:/FinReporter/FM_EXT.py:449: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
quote_df[''TVol''] = quote_df[''TVol'']/TVOL_SCALE
E:/FinReporter/FM_EXT.py:450: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
quote_df[''TAmt''] = quote_df[''TAmt'']/TAMT_SCALE
E:/FinReporter/FM_EXT.py:453: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
quote_df[''TDate''] = quote_df.TDate.map(lambda x: x[0:4]+x[5:7]+x[8:10])
Advertencia de copia de fotogramas de pandas
Cuando vas y haces algo como esto:
quote_df = quote_df.ix[:,[0,3,2,1,4,5,8,9,30,31]]
pandas.ix
en este caso devuelve un nuevo marco de datos independiente.
Cualquier valor que decida cambiar en este marco de datos, no cambiará el marco de datos original.
Esto es lo que los pandas intentan advertirte.
Por qué .ix
es una mala idea
El objeto .ix
intenta hacer más de una cosa, y para cualquiera que haya leído algo sobre código limpio, este es un olor fuerte.
Dado este marco de datos:
df = pd.DataFrame({"a": [1,2,3,4], "b": [1,1,2,2]})
Dos comportamientos:
dfcopy = df.ix[:,["a"]]
dfcopy.a.ix[0] = 2
Comportamiento uno: dfcopy
ahora es un marco de datos independiente. Cambiarlo no cambiará df
df.ix[0, "a"] = 3
Comportamiento dos: Esto cambia el marco de datos original.
Use .loc
en .loc
lugar
Los desarrolladores de pandas reconocieron que el objeto .ix
era bastante maloliente [especulativamente] y, por lo tanto, crearon dos nuevos objetos que ayudan en la adhesión y asignación de datos. (El otro ser .iloc
)
.loc
es más rápido porque no intenta crear una copia de los datos.
.loc
está destinado a modificar su marco de datos existente en el lugar, que es más eficiente en memoria.
.loc
es predecible, tiene un comportamiento.
La solución
Lo que está haciendo en su ejemplo de código es cargar un archivo grande con muchas columnas, y luego modificarlo para que sea más pequeño.
La función pd.read_csv
puede ayudarlo con mucho de esto y también hacer que la carga del archivo sea mucho más rápida.
Así que en lugar de hacer esto
quote_df = pd.read_csv(StringIO(str_of_all), sep='','', names=list(''ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg'')) #dtype={''A'': object, ''B'': object, ''C'': np.float64}
quote_df.rename(columns={''A'':''STK'', ''B'':''TOpen'', ''C'':''TPCLOSE'', ''D'':''TPrice'', ''E'':''THigh'', ''F'':''TLow'', ''I'':''TVol'', ''J'':''TAmt'', ''e'':''TDate'', ''f'':''TTime''}, inplace=True)
quote_df = quote_df.ix[:,[0,3,2,1,4,5,8,9,30,31]]
Hacer esto
columns = [''STK'', ''TPrice'', ''TPCLOSE'', ''TOpen'', ''THigh'', ''TLow'', ''TVol'', ''TAmt'', ''TDate'', ''TTime'']
df = pd.read_csv(StringIO(str_of_all), sep='','', usecols=[0,3,2,1,4,5,8,9,30,31])
df.columns = columns
Esto solo leerá las columnas que le interesan y las nombrará correctamente. No hay necesidad de usar el objeto malvado .ix
para hacer cosas mágicas.
El SettingWithCopyWarning
fue creado para marcar asignaciones "encadenadas" potencialmente confusas, como las siguientes, que no siempre funcionan como se espera, particularmente cuando la primera selección devuelve una copia . [vea GH5390 y GH5597 para una discusión de fondo.]
df[df[''A''] > 2][''B''] = new_val # new_val not set in df
La advertencia ofrece una sugerencia para volver a escribir de la siguiente manera:
df.loc[df[''A''] > 2, ''B''] = new_val
Sin embargo, esto no se ajusta a su uso, que es equivalente a:
df = df[df[''A''] > 2]
df[''B''] = new_val
Si bien está claro que no le importa que las escrituras vuelvan al marco original (ya que sobrescribió la referencia a él), desafortunadamente este patrón no se puede diferenciar del primer ejemplo de asignación encadenada, por lo tanto, la advertencia (falsa positiva). El potencial de falsos positivos se aborda en los documentos sobre indexación , si desea leer más. Puede deshabilitar esta nueva advertencia de forma segura con la siguiente asignación.
pd.options.mode.chained_assignment = None # default=''warn''
En general, el objetivo de SettingWithCopyWarning
es mostrar a los usuarios (y especialmente a los usuarios nuevos) que pueden estar operando en una copia y no en el original como piensan. Hay falsos positivos (IOW si sabes lo que estás haciendo podría estar bien ). Una posibilidad es simplemente desactivar (de forma predeterminada, la advertencia ) como sugiere @Garrett.
Aquí hay otra opción:
In [1]: df = DataFrame(np.random.randn(5, 2), columns=list(''AB''))
In [2]: dfa = df.ix[:, [1, 0]]
In [3]: dfa.is_copy
Out[3]: True
In [4]: dfa[''A''] /= 2
/usr/local/bin/ipython:1: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
#!/usr/local/bin/python
Puede establecer el indicador is_copy
en False
, que desactivará efectivamente la comprobación, para ese objeto :
In [5]: dfa.is_copy = False
In [6]: dfa[''A''] /= 2
Si copia explícitamente, no se producirá ninguna otra advertencia:
In [7]: dfa = df.ix[:, [1, 0]].copy()
In [8]: dfa[''A''] /= 2
El código que se muestra en el OP arriba, mientras que es legítimo, y probablemente algo que hago también, es técnicamente un caso para esta advertencia, y no un falso positivo. Otra forma de no tener la advertencia sería hacer la operación de selección a través de reindex
, por ejemplo,
quote_df = quote_df.reindex(columns=[''STK'', ...])
O,
quote_df = quote_df.reindex([''STK'', ...], axis=1) # v.0.21
Esto debería funcionar:
quote_df.loc[:,''TVol''] = quote_df[''TVol'']/TVOL_SCALE
Para eliminar cualquier duda, mi solución fue hacer una copia profunda de la porción en lugar de una copia normal. Es posible que esto no sea aplicable dependiendo de su contexto (restricciones de memoria / tamaño de la porción, potencial de degradación del rendimiento, especialmente si la copia se produce en un bucle como lo hizo para mí, etc.)
Para ser claros, aquí está la advertencia que recibí:
/opt/anaconda3/lib/python3.6/site-packages/ipykernel/__main__.py:54:
SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame
See the caveats in the documentation:
http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
Ilustración
Tuve dudas de que la advertencia fue lanzada debido a una columna que estaba dejando caer en una copia de la rebanada. Si bien técnicamente no se intentaba establecer un valor en la copia de la división, seguía siendo una modificación de la copia de la división. A continuación se encuentran los pasos (simplificados) que he tomado para confirmar la sospecha, espero que ayude a aquellos de nosotros que estamos tratando de entender la advertencia.
Ejemplo 1: colocar una columna en el original afecta a la copia
Ya lo sabíamos pero este es un recordatorio saludable. Esto NO es de lo que trata la advertencia.
>> data1 = {''A'': [111, 112, 113], ''B'':[121, 122, 123]}
>> df1 = pd.DataFrame(data1)
>> df1
A B
0 111 121
1 112 122
2 113 123
>> df2 = df1
>> df2
A B
0 111 121
1 112 122
2 113 123
# Dropping a column on df1 affects df2
>> df1.drop(''A'', axis=1, inplace=True)
>> df2
B
0 121
1 122
2 123
Es posible evitar que los cambios realizados en df1 afecten a df2.
>> data1 = {''A'': [111, 112, 113], ''B'':[121, 122, 123]}
>> df1 = pd.DataFrame(data1)
>> df1
A B
0 111 121
1 112 122
2 113 123
>> import copy
>> df2 = copy.deepcopy(df1)
>> df2
A B
0 111 121
1 112 122
2 113 123
# Dropping a column on df1 does not affect df2
>> df1.drop(''A'', axis=1, inplace=True)
>> df2
A B
0 111 121
1 112 122
2 113 123
Ejemplo 2: colocar una columna en la copia puede afectar al original
Esto realmente ilustra la advertencia.
>> data1 = {''A'': [111, 112, 113], ''B'':[121, 122, 123]}
>> df1 = pd.DataFrame(data1)
>> df1
A B
0 111 121
1 112 122
2 113 123
>> df2 = df1
>> df2
A B
0 111 121
1 112 122
2 113 123
# Dropping a column on df2 can affect df1
# No slice involved here, but I believe the principle remains the same?
# Let me know if not
>> df2.drop(''A'', axis=1, inplace=True)
>> df1
B
0 121
1 122
2 123
Es posible evitar que los cambios realizados en df2 afecten a df1.
>> data1 = {''A'': [111, 112, 113], ''B'':[121, 122, 123]}
>> df1 = pd.DataFrame(data1)
>> df1
A B
0 111 121
1 112 122
2 113 123
>> import copy
>> df2 = copy.deepcopy(df1)
>> df2
A B
0 111 121
1 112 122
2 113 123
>> df2.drop(''A'', axis=1, inplace=True)
>> df1
A B
0 111 121
1 112 122
2 113 123
¡Aclamaciones!
Para mí, este problema se produjo en un siguiente ejemplo> simplificado <. Y también pude resolverlo (con suerte con una solución correcta):
código antiguo con advertencia:
def update_old_dataframe(old_dataframe, new_dataframe):
for new_index, new_row in new_dataframe.iterrorws():
old_dataframe.loc[new_index] = update_row(old_dataframe.loc[new_index], new_row)
def update_row(old_row, new_row):
for field in [list_of_columns]:
# line with warning because of chain indexing old_dataframe[new_index][field]
old_row[field] = new_row[field]
return old_row
Esto imprimió la advertencia para la línea old_row[field] = new_row[field]
Dado que las filas en el método update_row son en realidad de tipo Series
, reemplacé la línea con:
old_row.at[field] = new_row.at[field]
Es decir, method para acceder / buscar una Series
. Aunque ambos funcionan bien y el resultado es el mismo, de esta manera no tengo que deshabilitar las advertencias (= guardarlas para otros problemas de indexación de cadenas en otro lugar).
Espero que esto pueda ayudar a alguien.
Podrías evitar todo este problema, creo yo:
return (
pd.read_csv(StringIO(str_of_all), sep='','', names=list(''ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg'')) #dtype={''A'': object, ''B'': object, ''C'': np.float64}
.rename(columns={''A'':''STK'', ''B'':''TOpen'', ''C'':''TPCLOSE'', ''D'':''TPrice'', ''E'':''THigh'', ''F'':''TLow'', ''I'':''TVol'', ''J'':''TAmt'', ''e'':''TDate'', ''f'':''TTime''}, inplace=True)
.ix[:,[0,3,2,1,4,5,8,9,30,31]]
.assign(
TClose=lambda df: df[''TPrice''],
RT=lambda df: 100 * (df[''TPrice'']/quote_df[''TPCLOSE''] - 1),
TVol=lambda df: df[''TVol'']/TVOL_SCALE,
TAmt=lambda df: df[''TAmt'']/TAMT_SCALE,
STK_ID=lambda df: df[''STK''].str.slice(13,19),
STK_Name=lambda df: df[''STK''].str.slice(21,30)#.decode(''gb2312''),
TDate=lambda df: df.TDate.map(lambda x: x[0:4]+x[5:7]+x[8:10]),
)
)
Usando Asignar. De la documentation : Asigne nuevas columnas a un DataFrame, devolviendo un nuevo objeto (una copia) con todas las columnas originales además de las nuevas.
Vea el artículo de Tom Augspurger sobre el encadenamiento de métodos en pandas: https://tomaugspurger.github.io/method-chaining
Si ha asignado la división a una variable y desea establecerla utilizando la variable de la siguiente manera:
df2 = df[df[''A''] > 2]
df2[''B''] = value
Y no desea utilizar la solución de Jeff porque su condición informática df2
es demasiado larga o por alguna otra razón, entonces puede usar lo siguiente:
df.loc[df2.index.tolist(), ''B''] = value
df2.index.tolist()
devuelve los índices de todas las entradas en df2, que luego se utilizarán para establecer la columna B en el marco de datos original.