<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Módulo 2
Step1:  1. Series
Una de las estructuras básicas de Pandas es la serie
Step2:  También podemos crear una Series a partir de un diccionario de Python. Como no le especificamos índices, se genera a partir de las primeras componentes, ordenadas en el mismo orden de inserción en el diccionario
Step3:  Podemos especificar un índice para indicar el orden (y para meter elementos inexistentes). La forma estándar en Pandas de especificar la ausencia de datos es vía NaN.
Las series se comportan de forma muy similar a un array y, de hecho, la mayoría de las operaciones con ndarrays admiten series como argumentos (y manejan apropiadamente las etiquetas, para que sigan asociadas luego de realizada la operación)
Step4:  Alternativamente, podemos ver a las series como un diccionario (de largo fijo) que puede accederse y cambiar valores a través de su índice
Step5:  Al igual que en NumPy, las series admite operaciones vectorizadas. También es interesante ver que las operaciones sobre Series alinean en base a las etiquetas automáticamente (utilizando la unión de las etiquetas de las series involucradas). Cuando una etiqueta está en una serie pero no en la otra, el resultado se marca como NaN.
Step6:  Las Series tienen un nombre, que está en el atributo name, y que puede especificarse al crearlo, o cambiarse con rename()
Step7:  La función value_counts es muy interesante, porque, dada una Series,  nos devuelve una Series con la cantidad de valores diferentes (en nuestro ejemplo es trivial, porque todos los valores son diferentes).
Step8:  2. DataFrames
Los DataFrames son la estructura más comúnmente utilizada en pandas. Pueden verse como un conjunto de columnas de diferentes tipos (como una planilla Excel), o como una matriz 2D con etiquetas asociadas. Al crearlas, se pueden especificar los "index" (etiquetas de las filas), y/o los "columns" (las etiquetas de las columnas).
Existen muchas formas de crear DataFrames
Step9:  Obsérvese que los nombres de los index y los columns son creados automáticamente, pero probablemente querramos especificarlos en la creación. En el ejemplo anterior, nos gustaría ponerle nombres a las columnas (en este caso, cada fila tiene las características de un arma en el juego Call of Duty)
Step10:  Podemos consultar los índices y las columnas
Step11:  Veamos otra forma de crear DataFrames
Step12:  Los arrays son objetos, y tienen métodos asociados. Utilice el método dtype para conocer el tipo de los elementos de a
En la documentación pueden verse muchas formas de crear DataFrames. Si creamos un DataFrame a partir de una Series, obtendremos una sola columna, cuyo nombre es el nombre de la Series. 
Una forma de tener una idea general sobre nuestro DataFrame es utilizando el método describe
Step13:  3.Operaciones básicas con DataFrames
Los DataFrames pueden verse (como dijimos antes) como diccionarios de Series, indexados por los nombres de las columnas. Pueden accederse y modificarse igual que los diccionarios comunes.
Step14:  Es posible calcular funciones numéricas sobre algunas columnas
Step15:  Para borrar una columna, usamos del
Step16:  Si los valores que se pasan para crear una columna no son suficientes, se completan con NaN
Step17:  Como vimos, seleccionar una columna de un DataSeries es muy parecido a seleccionar un elemento de un diccionario, siendo la clave el nombre de la columna (también es posible seleccionar por más de una columna a la vez
Step18:  Vamos a agregarle a nuestro DataFrame los nombres de las armas, y lo ponemos como índice.
Step19:  Para seleccionar una fila, existen varias formas diferentes. Si conocemos su index, utilizamos loc
Step20:  Si conocemos el índice de su posición, utilizamos iloc
Step21:  Podemos hacer slicing de las filas igual que con los ndarrays, utilizando un rango en la selección (obsérvese que aquí se busca en las filas, no en las columnas, y que se devuelve un DataFrame)
Step22:  Podemos seleccionar de un dataframe las celdas que cumplan cierta condición (igual que se podía hacer con los arrays), y utilizar el resultado para seleccionar celdas que cumplan la condición (aquí se marcarán con NaN las celdas que no hayan sido seleccionadas).
Step23:  Cuando se realizan operaciones entre DataFrames, al igual que con Series, se alinean tanto las indexes como las columns, devolviéndose siempre la unión de los indexes/columns de los DataFrames involucrados.
Por ejemplo, agreguemos algunas armas más a nuestra base de datos, utilizando el método append (creamos primero un nuevo DataFrame, y luego lo concatenamos al original, para obtener el nuevo DataFrame). En el nuevo DataFrame, agregaremos una columna Dummy para ver qué sucede en la concatenación.
Step24:  En la siguiente operaciones, vamos a restarle una Series al DataFrame. En ese caso, pandas alinea las columnas con los ìndices de la Series, y eso hace que se resten todos los elementos de la fila.
Step25:  Los DF se pueden multiplicar por escalares, y se pueden aplicar operadores booleanos, exactamente igual que a los ndarrays.
Step26:  4. Operaciones de selección avanzada.
En esta sección veremos formas de seleccionar columnas y filas de acuerdo a diferentes condiciones, e incluso a agruparlas (de forma similar a lo que se hace con SQL).
4.1 Selección de columnas
Es posible seleccionar todos los elementos de una o más columnas utilizando una lista de columnas como argumento. (El método head simplemente selecciona las primeras filas del resultado).
Step27:  4.2 Selección de filas por condición booleana
Si queremos poner una condición (como en la cláusula WHERE de SQL), utilizaremos la selección por valores Booleanos, que surgirán de una condición. Por ejemplo, para obtener todas las columnas de las armas con precisión mayor a 50
Step28:  Lo que estamos haciendo es construir una Series de valores booleanos, para que me devuelva todas las filas que tienen True
Step29:  ... y luego pasamos esta Series como argumento para seleccionar aquellas filas que valen True.
Para agregar más condiciones, podemos utilizar los operadores booleanos & (AND) y | (OR). Atención
Step30:  Creemos un nuevo DataFrame que tenga, para cada arma, su tipo. Para eso, construimos una Series a partir de un diccionario (donde los índices coincidan con los que tenemos), y simplemente lo asignamos a nueva nueva columna del DataFrame ya construido.
Step31:  Listemos solamente los Fusiles de Precisión
Step32:  Es posible ordenar los resultados de una consulta (que es siempre un DataFrame)
Step33:  4.3 Operaciones sobre conjuntos de filas
Si lo que queremos es agrupar las filas de acuerdo al valor de una o más columnas (u otro criterio), en forma similar a la operación GROUP BY de SQL, podemos utilizar el método groupby. Por ejemplo, si queremos conocer la precisión promedio según el tipo de arma, primero agrupamos por tipo, luego seleccionamos la Series que nos interesa, y finalmente le aplicamos la operación mean
Step34:  Si queremos obtener la medida de todas las columnas, usamos agg para indicarle que aplique el método np.mean a todas las columnas (el método permite más de una función, así que calcularemos también la desviación estándar). En nuestro ejemplo, estamos agrupando según el valor de una sola columna, pero puede agruparse por más de una.
Step35:  Podemos aplicar diferentes funciones a diferentes columnas...
Step36:  Y podemos también aplicarlo a todas las filas de nuestro DataFrame
Step37:  4.4 Más selección de elementos
Step38:  También podemos especificar más de una fila, utilizando una lista (nótese que es una lista dentro de otra)
Step39:  Si queremos obtener una celda, el primer elemento de loc nos selecciona la fila, y el segundo la columna
Step40:  Podemos setear el valor de una celda utilizando loc
Step41:  Finalmente, podemos especificar slices, indicando, en cada axis, el primer y último elemento (que estarán incluidos).
Step42:  Alternativamente, el método iloc permite hacer lo mismo, pero especifiando posiciones enteras, en lugar de labels (de forma similar a como lo hace NumPy)
5. Importar datos desde archivos
Por supuesto, en entornos de trabajo reales, donde hay miles o millones de instancias, no se cargan los dataframes "a mano"", sino que se importan desde archivos (separados por comas, o en algúún tipo de XML, o en formato json). Pandas provee diferentes formas de importar. En nuestro caso, importaremos los datos de las armas desde un archivo csv.
Step43:  ¿Cuál es la mejor arma para un francotirador? Ordenamos por el promedio entre alcance y precisión
<ASSISTANT_TASK:>
Python Code:
import numpy as np
import pandas as pd
# Este notebook fue elaborado con la versión 1.0.3 de Pandas
pd.__version__
Explanation: Módulo 2: Pandas, análisis de datos con Python
En este notebook describiremos las características principales, y formas de trabajo con Pandas, la principal biblioteca de análisis de datos del ecosistema Python. Pandas está apoyado en NumPy, la biblioteca de análisis numérico básica de Python (este módulo asume que el lector conoce NumPy).
Referencias:
El notebook está basado principalmente en los tutoriales disponibles en la documentación de Pandas.
Para la comparación con SQL, es muy útil esta sección en particular de la documentación
Muy buenos ejercicios que cubren aspectos aquí presentados (y algunos más).
End of explanation
s= pd.Series(np.random.randn(5), index=['a','b','c','d','e'])
s
Explanation: 1. Series
Una de las estructuras básicas de Pandas es la serie: un array unidimensional etiquetado que puede contener cualquier tipo de datos de Python. Atención: en Pandas los datos y sus etiquetas van siempre juntos, a menos que esa relación se quiebre a propósito. Las etiquetas son llamadas en general index. Las Series pueden crearse a partir de ndarrays, diccionarios de Python o valores escalares.
Las series se comportan de forma muy similar a un ndarray, y son argumentos válidos de la mayoría de las funciones de NumPy. 
Creemos una serie de 5 números aleatorios, cada uno con su etiqueta asociada (el largo del índice debe ser el mismo que el del array). Si no se le indica índice, le va a poner [0, ..., len(data)-1]
End of explanation
d = pd.Series({'b': 1, 'a': 0, 'c': 2})
d
Explanation: También podemos crear una Series a partir de un diccionario de Python. Como no le especificamos índices, se genera a partir de las primeras componentes, ordenadas en el mismo orden de inserción en el diccionario:
End of explanation
s[s > s.median()] # Seleccionamos los valores mayores a la mediana del array. 
Explanation: Podemos especificar un índice para indicar el orden (y para meter elementos inexistentes). La forma estándar en Pandas de especificar la ausencia de datos es vía NaN.
Las series se comportan de forma muy similar a un array y, de hecho, la mayoría de las operaciones con ndarrays admiten series como argumentos (y manejan apropiadamente las etiquetas, para que sigan asociadas luego de realizada la operación):
End of explanation
s['a']
s['e']=12
s
'e' in s
s.get(['f'],np.nan) # Si no ponemos el get, devuelve error
Explanation: Alternativamente, podemos ver a las series como un diccionario (de largo fijo) que puede accederse y cambiar valores a través de su índice:
End of explanation
s[1:] # sin el primer elemento
s[:-1] # sin el último elemento
s[1:]+s[:-1]
Explanation: Al igual que en NumPy, las series admite operaciones vectorizadas. También es interesante ver que las operaciones sobre Series alinean en base a las etiquetas automáticamente (utilizando la unión de las etiquetas de las series involucradas). Cuando una etiqueta está en una serie pero no en la otra, el resultado se marca como NaN.
End of explanation
s2=s.rename('My_index')
s2
Explanation: Las Series tienen un nombre, que está en el atributo name, y que puede especificarse al crearlo, o cambiarse con rename()
End of explanation
s.value_counts()
Explanation: La función value_counts es muy interesante, porque, dada una Series,  nos devuelve una Series con la cantidad de valores diferentes (en nuestro ejemplo es trivial, porque todos los valores son diferentes).
End of explanation
a = np.array([
    [65,60,60,45,60],
    [75,35,50,75,40],
    [85,80,30,20,75],
    [75,45,30,70,80],
    [80,55,90,40,45],
    [90,60,95,15,45],
    [60,55,45,55,40]
])
df=pd.DataFrame(a)
df
Explanation: 2. DataFrames
Los DataFrames son la estructura más comúnmente utilizada en pandas. Pueden verse como un conjunto de columnas de diferentes tipos (como una planilla Excel), o como una matriz 2D con etiquetas asociadas. Al crearlas, se pueden especificar los "index" (etiquetas de las filas), y/o los "columns" (las etiquetas de las columnas).
Existen muchas formas de crear DataFrames: como un diccionario de Series o ndarrays, ndarrays de 2 dimensiones, una Serie o incluso otro DataFrame. 
Creemos un DataFrame a partir de un 2D-ndarray:
End of explanation
df=pd.DataFrame(a,columns=['Daño','Precisión','Alcance','Cadencia','Movilidad'])
df
Explanation: Obsérvese que los nombres de los index y los columns son creados automáticamente, pero probablemente querramos especificarlos en la creación. En el ejemplo anterior, nos gustaría ponerle nombres a las columnas (en este caso, cada fila tiene las características de un arma en el juego Call of Duty):
End of explanation
df.index, df.columns
Explanation: Podemos consultar los índices y las columnas:
End of explanation
d = {'one': pd.Series([1., 2., 3.], index=['a', 'b', 'c']),
     'two': pd.Series([1., 2., 3., 4.], index=['a', 'b', 'c', 'd'])} 
# Obsérvese que en la columna 'one' no tenemos nada en la fila 'd'
df2 = pd.DataFrame(d)
df2
pd.DataFrame(d, columns=['two','three'])
Explanation: Veamos otra forma de crear DataFrames: a través de una lista de Series. Obsérvese qué pasa cuando se especifica un índice que no está en el diccionario.
End of explanation
df.describe()
Explanation: Los arrays son objetos, y tienen métodos asociados. Utilice el método dtype para conocer el tipo de los elementos de a
En la documentación pueden verse muchas formas de crear DataFrames. Si creamos un DataFrame a partir de una Series, obtendremos una sola columna, cuyo nombre es el nombre de la Series. 
Una forma de tener una idea general sobre nuestro DataFrame es utilizando el método describe
End of explanation
df['Precisión']
df['Dummy']=df['Alcance']*df['Daño']
df['Es_preciso']=df['Precisión'] >= 60
df
Explanation: 3.Operaciones básicas con DataFrames
Los DataFrames pueden verse (como dijimos antes) como diccionarios de Series, indexados por los nombres de las columnas. Pueden accederse y modificarse igual que los diccionarios comunes.
End of explanation
df[['Precisión', 'Alcance']].mean()
Explanation: Es posible calcular funciones numéricas sobre algunas columnas:
End of explanation
del df['Dummy']
df
Explanation: Para borrar una columna, usamos del:
End of explanation
df2['one_trunc'] = df2['one'][:2] 
df2
Explanation: Si los valores que se pasan para crear una columna no son suficientes, se completan con NaN
End of explanation
df['Precisión']
df[['Precisión', 'Alcance']]
Explanation: Como vimos, seleccionar una columna de un DataSeries es muy parecido a seleccionar un elemento de un diccionario, siendo la clave el nombre de la columna (también es posible seleccionar por más de una columna a la vez: en ese caso, en vez de pasarle el nombre de la columna, le pasamos una lista con los nombres de las columnas seleccionadas)
End of explanation
df['Arma']=['M16 Evil Clown', 'S36 Evil Clown', 'BY15 SnowFlakes', 'MSMC Ancient Runes', 
            'XPR-50 April\'s Fool', 'DLQ33 DeepShark', 'M4LMG RibbonExplosion']
df.set_index('Arma', inplace=True)
df
df.index # El índice ahora cambió
Explanation: Vamos a agregarle a nuestro DataFrame los nombres de las armas, y lo ponemos como índice.
End of explanation
df.loc['BY15 SnowFlakes']
Explanation: Para seleccionar una fila, existen varias formas diferentes. Si conocemos su index, utilizamos loc:
End of explanation
df.iloc[0]
Explanation: Si conocemos el índice de su posición, utilizamos iloc
End of explanation
df[1:3]
Explanation: Podemos hacer slicing de las filas igual que con los ndarrays, utilizando un rango en la selección (obsérvese que aquí se busca en las filas, no en las columnas, y que se devuelve un DataFrame):
End of explanation
df>50
df[df>50] 
del df['Es_preciso']
Explanation: Podemos seleccionar de un dataframe las celdas que cumplan cierta condición (igual que se podía hacer con los arrays), y utilizar el resultado para seleccionar celdas que cumplan la condición (aquí se marcarán con NaN las celdas que no hayan sido seleccionadas).
End of explanation
d = np.array([[85,52,95,30,50,-1],[80,55,90,40,45,-1],[65,60,60,45,60,-1],[85,52,95,30,50,-1],[48,65,90,63,60,-1],
              [60,55,45,55,40,-1]
             ,[78,55,32,60,75,-1],[90,40,25,60,75,-1]])
arm_names=['Arctic.50 Bats','XPR-50 RedTriangle','M16 NeonTiger', 'Arctic.50 RedTriangle','BK57 JackFrost',
                           'M4MLG RedTriangle', 'AKS-74U NeonTiger','PDW-57 ZombieGene']
df2= pd.DataFrame(d,index=arm_names, columns=['Daño','Precisión','Alcance','Cadencia','Movilidad','Dummy'])
df3= df.append(df2, sort=False)
df3
del df3['Dummy']
Explanation: Cuando se realizan operaciones entre DataFrames, al igual que con Series, se alinean tanto las indexes como las columns, devolviéndose siempre la unión de los indexes/columns de los DataFrames involucrados.
Por ejemplo, agreguemos algunas armas más a nuestra base de datos, utilizando el método append (creamos primero un nuevo DataFrame, y luego lo concatenamos al original, para obtener el nuevo DataFrame). En el nuevo DataFrame, agregaremos una columna Dummy para ver qué sucede en la concatenación.
End of explanation
df3.iloc[0]
df3 - df3.iloc[0]
Explanation: En la siguiente operaciones, vamos a restarle una Series al DataFrame. En ese caso, pandas alinea las columnas con los ìndices de la Series, y eso hace que se resten todos los elementos de la fila.
End of explanation
df3['Movilidad']*1.5
Explanation: Los DF se pueden multiplicar por escalares, y se pueden aplicar operadores booleanos, exactamente igual que a los ndarrays.
End of explanation
df3[['Daño','Precisión']].head(5)
Explanation: 4. Operaciones de selección avanzada.
En esta sección veremos formas de seleccionar columnas y filas de acuerdo a diferentes condiciones, e incluso a agruparlas (de forma similar a lo que se hace con SQL).
4.1 Selección de columnas
Es posible seleccionar todos los elementos de una o más columnas utilizando una lista de columnas como argumento. (El método head simplemente selecciona las primeras filas del resultado).
End of explanation
df3[df3['Precisión']>50]
Explanation: 4.2 Selección de filas por condición booleana
Si queremos poner una condición (como en la cláusula WHERE de SQL), utilizaremos la selección por valores Booleanos, que surgirán de una condición. Por ejemplo, para obtener todas las columnas de las armas con precisión mayor a 50:
End of explanation
df3['Precisión']>50
Explanation: Lo que estamos haciendo es construir una Series de valores booleanos, para que me devuelva todas las filas que tienen True:
End of explanation
df3[df3['Precisión']>50][['Alcance', 'Cadencia']]
df3
Explanation: ... y luego pasamos esta Series como argumento para seleccionar aquellas filas que valen True.
Para agregar más condiciones, podemos utilizar los operadores booleanos & (AND) y | (OR). Atención: los paréntesis son obligatorios porque tiene más precedencia el == que el &! Seleccionemos todas las armas que tienen precisión o daño mayores que 80:
Si queremos seleccionar solamente alguna de las columnas Y algunas de las filas, podemos especificarlas en el DataFrame resultado de la operación anterior.
End of explanation
d={ 'M16 Evil Clown':'Fusil de Asalto', 
    'S36 Evil Clown':'Ametralladora', 
    'BY15 SnowFlakes':'Escopeta',
    'MSMC Ancient Runes':'Ametralladora Ligera', 
    'XPR-50 April\'s Fool':'Fusil de Precisión',
    'DLQ33 DeepShark':'Fusil de Precisión',
    'M4LMG RibbonExplosion':'Ametralladora',
    'Arctic.50 Bats':'Fusil de Precisión',
    'XPR-50 RedTriangle':'Fusil de Precisión',
    'M16 NeonTiger':'Fusil de Asalto', 
    'Arctic.50 RedTriangle':'Fusil de Precisión',
    'BK57 JackFrost':'Fusil de Asalto',
    'M4MLG RedTriangle':'Ametralladora Ligera', 
    'AKS-74U NeonTiger':'Subfusil',
    'PDW-57 ZombieGene':'Subfusil'}
df3['Tipo']=pd.Series(d)
df3
Explanation: Creemos un nuevo DataFrame que tenga, para cada arma, su tipo. Para eso, construimos una Series a partir de un diccionario (donde los índices coincidan con los que tenemos), y simplemente lo asignamos a nueva nueva columna del DataFrame ya construido.
End of explanation
df3[df3['Tipo']=='Fusil de Precisión']
Explanation: Listemos solamente los Fusiles de Precisión
End of explanation
df3[df3['Tipo']=='Fusil de Precisión'].sort_values(['Daño', 'Precisión'])
Explanation: Es posible ordenar los resultados de una consulta (que es siempre un DataFrame):
End of explanation
df3.groupby('Tipo')['Precisión'].mean()
Explanation: 4.3 Operaciones sobre conjuntos de filas
Si lo que queremos es agrupar las filas de acuerdo al valor de una o más columnas (u otro criterio), en forma similar a la operación GROUP BY de SQL, podemos utilizar el método groupby. Por ejemplo, si queremos conocer la precisión promedio según el tipo de arma, primero agrupamos por tipo, luego seleccionamos la Series que nos interesa, y finalmente le aplicamos la operación mean
End of explanation
df3.groupby('Tipo').agg([np.mean, np.std])
Explanation: Si queremos obtener la medida de todas las columnas, usamos agg para indicarle que aplique el método np.mean a todas las columnas (el método permite más de una función, así que calcularemos también la desviación estándar). En nuestro ejemplo, estamos agrupando según el valor de una sola columna, pero puede agruparse por más de una.
End of explanation
df3.groupby('Tipo').agg({'Daño':[np.mean, np.std], 'Alcance':[np.mean]})
Explanation: Podemos aplicar diferentes funciones a diferentes columnas...
End of explanation
df3.agg([np.mean, np.std])
df3['Precisión'].agg('mean')
Explanation: Y podemos también aplicarlo a todas las filas de nuestro DataFrame:
End of explanation
df3.loc['PDW-57 ZombieGene']
Explanation: 4.4 Más selección de elementos:  loc e iloc revisados
El atributo loc (observar que no es un método!) permite seleccionar partes (slices) de un DataFrame, utilizando los indexes o las columnas. 
Por ejemplo, como vimos antes, podemos utilizarlo para seleccionar un arma (fila) si tenemos su nombre, y nos devuelve una Series con todos los valores de cada columna de esa fila.
End of explanation
df3.loc[['XPR-50 RedTriangle', 'PDW-57 ZombieGene']]
Explanation: También podemos especificar más de una fila, utilizando una lista (nótese que es una lista dentro de otra)
End of explanation
df3.loc['XPR-50 RedTriangle', 'Alcance']
Explanation: Si queremos obtener una celda, el primer elemento de loc nos selecciona la fila, y el segundo la columna:
End of explanation
df3.loc['XPR-50 RedTriangle', 'Alcance'] *=2
df3.loc[['XPR-50 RedTriangle', 'PDW-57 ZombieGene']]
Explanation: Podemos setear el valor de una celda utilizando loc
End of explanation
df3.loc[['XPR-50 RedTriangle', 'PDW-57 ZombieGene'], 'Daño':'Alcance']
Explanation: Finalmente, podemos especificar slices, indicando, en cada axis, el primer y último elemento (que estarán incluidos).
End of explanation
df4=pd.read_csv('https://raw.githubusercontent.com/gmonce/datascience/master/data/call_of_duty.csv')
df4
df4.set_index('Arma', inplace=True)
df4
Explanation: Alternativamente, el método iloc permite hacer lo mismo, pero especifiando posiciones enteras, en lugar de labels (de forma similar a como lo hace NumPy)
5. Importar datos desde archivos
Por supuesto, en entornos de trabajo reales, donde hay miles o millones de instancias, no se cargan los dataframes "a mano"", sino que se importan desde archivos (separados por comas, o en algúún tipo de XML, o en formato json). Pandas provee diferentes formas de importar. En nuestro caso, importaremos los datos de las armas desde un archivo csv.
End of explanation
df4['Alc&Prec']=(df4['Alcance']+ df4['Precision']+ df4['Daño'])/3
df4.sort_values('Alc&Prec', ascending=False)
# Ahora promediamos el resto, para los tiradores de cerca
# Elmino las que hacen poco daño o  tienen poca cadencia
df4['Cerca']=(df4['Cadencia']+ df4['Alcance']+ df4['Daño']+ df4['Movilidad'])/4
df4[(df4['Daño']>50) & (df4['Cadencia']>50)].sort_values('Cerca', ascending=False)
Explanation: ¿Cuál es la mejor arma para un francotirador? Ordenamos por el promedio entre alcance y precisión
End of explanation
<END_TASK>