<jupyter_start><jupyter_text>Estructuras de Datos  PANDAS MANEJA ESTRUCTURAS DE DATOS ESPECIALES PARA REPRESENTAR TABLAS Y OTROS TIPOS DE DATOS ORDENADOS  Series<jupyter_code># Es un objeto que representa una serie de datos conectados a una descripción de los mismos
obj = pd.Series([4, 7, -5, 3])
obj
# ATRIBUTOS DE UN OBJETO SERIES

# Valores que guarda
obj.values

# Etiqueta que identifica a cada valor del Series
obj.index 
# El Index puede ser especificado en la definición
obj2 = pd.Series([4, 7, -5, 3], index=['d', 'b', 'a', 'c'])
obj2
# Podemos seleccionar/asignar un valor de un Series haciendo referencia a su índice

obj2['a'] # Los índices son strings...
obj2[2] # o enteros

obj2['d'] = 2 # Asignación

obj2[['c', 'a', 'd']] # Fancy indexing
# Podemos realizar operaciones que realizamos con ndarrays de NumPy
obj2[obj2 > 0] # Boolean indexing

obj2 * 2 # Multiplicación escalar

np.exp(obj2) # Aplicar ufuncs
# Podemos crear Series desde diccionarios
sdata = {'Ohio': 35000, 'Texas': 71000, 'Oregon': 16000, 'Utah': 5000}
obj3 = pd.Series(sdata)
obj3
# Incluso podemos especificar el orden en el que queremos que estén los índices
states = ['California', 'Ohio', 'Oregon', 'Texas']
obj4 = pd.Series(sdata, index=states) # California aparece como NaN
obj4
# Podemos hacer operaciones entre Series
obj3
obj4
obj3+obj4 # Los índices se ordenan automáticamente
# Podemos cambiar el nombre tanto de las etiquetas como de los valores del Index
obj4.name = "population" # ¿Qué estamos guardando?
obj.index.name = "state" # ¿A qué pertenecen estos valores?
obj4<jupyter_output><empty_output><jupyter_text>DataFrame<jupyter_code># CREACIÓN DE DATAFRAMES
# Representan tablas de datos
data = {'state': ['Ohio', 'Ohio', 'Ohio', 'Nevada', 'Nevada', 'Nevada'],
       'year': [2000, 2001, 2002, 2001, 2001, 2003],
       'pop': [1.5, 1.7, 3.6, 2.4, 2.9, 3.2]}
frame = pd.DataFrame(data)
frame
# Podemos especificar el orden de las columnas al definir un DataFrame
pd.DataFrame(data, columns=['year', 'state', 'pop'])
# Igualmente podemos especificar el orden de los índices
frame2 = pd.DataFrame(data, columns=['year', 'state', 'pop', 'debt'],
                     index=['one', 'two', 'three', 'four', 'five', 'six'])
frame2
# ACCESO Y ASIGNACIÓN
# Podmeos acceder a una columna de dos maneras_
# Por etiqueta
frame2['state']
# Como atributo
frame2.state
# Podemos asignar valores al DataFrame
frame2
frame2['debt'] = 16.6 # Asignación de escalares...
frame2
frame2['debt'] = np.arange(6.) # o de arrays (¡cuidado con la longitud!)
frame2
# NUEVAS COLUMNAS
# Si asignamos a una columna inexsistente, se creará
frame2['eastern'] = frame2.state == 'Ohio'
frame2
# Podemos remover las columnas usando la palabra clave "del"
del frame2['eastern']
frame2.columns # Atributo de la clase DataFrame (es un Index)
# ATRIBUTOS DE UN DATAFRAME
pop = {
    'Nevada': {2001: 2.4, 2002: 2.9},
    'Ohio': {2000: 1.5, 2001: 1.7, 2002: 3.6}
}
frame3 = pd.DataFrame(pop)
frame3
# Atributos "Nombre del índice" y "Nombre de las columnas"
frame3.index.name = 'year'; frame3.columns.name = 'state'
frame3.values # Sólo los valores
frame3 # Valores y etiquetas<jupyter_output><empty_output><jupyter_text>Operaciones con Estructuras de Datos  Indexing<jupyter_code>obj = pd.Series(np.arange(4.), index=['a', 'b', 'c', 'd'])
obj
# EXISTEN MUCHAS FORMAS DE REFERENCIAR UN VALOR
obj['b'] # Por etiqueta
obj[1] # Por número entero (por posición)
obj[1:3] # Slicing de posiciones (el último valor NO se incluye)
obj['b':'d'] # Slicing de etiquetas (el último valor se incluye)
obj[['b', 'a', 'd']] # Por lista de etiquetas
obj[obj < 2] # Por indexado booleano ("boolean indexing")
# Asignar a un slice modifica el Series original
obj['b': 'c'] = 5
obj
# INDEXING EN DATAFRAMES
data = pd.DataFrame(np.arange(16).reshape((4,4)),
                   index=['Ohio', 'Colorado', 'Utah', 'New York'],
                   columns=['one', 'two', 'three', 'four'])
data
# Básicamente es lo mismo...
data['two'] # Por etiqueta
data[['three', 'one']] # BPor lista de etiquetas
data[:2] # Slicing
data[data['three']>5] # Indexado booleano (máscara es un Series)
# También podemos asignar utilizando cualquiera de los métodos anteriores
data[data < 5] = 0
data<jupyter_output><empty_output><jupyter_text>Selection (loc e iloc)<jupyter_code># EXISTE UN PROBLEMA: ¿Y SI TENEMOS ETIQUETAS QUE SON NUMÉRICAS?
# ¿HAGO INDEXING POR ETIQUETA O POR NÚMERO?
# UNA BUENA PRÁCTICA ES UTILIZAR LOC() E ILOC()
data
# loc() selecciona por etiquetas
data.loc['Colorado', ['two', 'three']]
# iloc() is para selección por enteros/por posición
data.iloc[2, [3, 0, 1]]
data.iloc[[1, 2, 3], [3, 0, 1]] # No es Fancy Indexing
# TAMBIÉN PODEMOS UTILIZAR SLICES CON ESTAS FUNCIONES
data.loc[:'Utah', 'two']
# No podemos utilizar indexado booleano dentro de iloc
data.iloc[:, :3][data.three > 5] # Filtramos > 5 después<jupyter_output><empty_output><jupyter_text>Reindexing<jupyter_code># REINDEX EN SERIES
# Podemos definir el índice al crear
obj = pd.Series([4.5, 7.2, -5.3, 3.6], index=['d', 'b', 'a', 'c'])
obj
# reindex devuelve un objeto con los índices en un orden escpecífico
obj2 = obj.reindex(['a', 'b', 'c', 'd', 'e']) # Nota: Valores NaN debido a que "e" no existe
obj2
# Podemos lidiar con los valores NaN de la siguiente manera
obj3 = pd.Series(['blue', 'purple', 'yellow'], index=[0, 2, 4])
obj3
# La opción ffill permite copiar valores no NaN anteriores
obj3.reindex(range(6), method='ffill')
# REINDEX EN DATAFRAMES
frame = pd.DataFrame(np.arange(9).reshape((3, 3)),
                    index=['a', 'c', 'd'],
                    columns=['Ohio', 'Texas', 'California'])
frame
# Hacer reindex en el índice es los mismo
frame2 = frame.reindex(['a', 'b', 'c', 'd'])
frame2
# Aunque tenemos la posibilidad de hacer reindex por columnas
states = ['Texas', 'Utah', 'California']
frame.reindex(columns=states)<jupyter_output><empty_output><jupyter_text>Dropping<jupyter_code># DROPPING EN SERIES
obj = pd.Series(np.arange(5), index=['a', 'b', 'c', 'd', 'e'])
obj
# drop crea un nuevo objeto sin el valor cuyo índice se especificó
new_obj = obj.drop('c') # Elimina valor con índice c
new_obj
obj.drop(['d', 'c']) # Elimina valores con índices d y c
# DROPPING EN DATAFRAMES
data = pd.DataFrame(np.arange(16).reshape((4,4)),
                   index=['Ohio', 'Colorado', 'Utah', 'New York'],
                   columns=['one', 'two', 'three', 'four'])
data
# Eliminar filas (por default)
data.drop(['Colorado', 'Ohio'])
# Eliminar columnas
data.drop('two', axis=1)
data.drop(['two', 'four'], axis='columns')
# ¡EL DROP PUEDE NO CREAR UN OBJETO NUEVO!
obj # Antes
obj.drop('c', inplace=True) # Opción inplace=True
obj # Después<jupyter_output><empty_output><jupyter_text>Ordenamiento/Sorting<jupyter_code># SORTING EN SERIES
obj = pd.Series(range(4), index=['d', 'a', 'b', 'c'])
obj.sort_index() # Ordena lexicográficamente
# SORTING EN DATAFRAMES
frame = pd.DataFrame(np.arange(8).reshape((2,4)),
                    index=['three', 'one'],
                    columns=['d', 'a', 'b', 'c'])
frame.sort_index() # Ordenar filas (por default)
frame.sort_index(axis=1) # Ordenar columnas
# Cambia el orden de ordenamiento (ascendente por defecto)
frame.sort_index(axis=1, ascending=False)
# ORDENAR POR VALORES, NO POR ÍNDICE
obj = pd.Series([4, 7, -3, 2])
obj.sort_values()
# En DataFrames, ordenamos usando una columna como criterio
frame = pd.DataFrame({'b': [4, 7, -3, 2], 'a': [0, 1, 0, 1]})
frame
frame.sort_values(by='b')<jupyter_output><empty_output><jupyter_text>Aplicación de Funciones<jupyter_code># UFUNCS
# Podemos utilizar las ufuncs de Numpy en Series y DataFrames
frame = pd.DataFrame(np.random.randn(4, 3), columns=list('bde'),
                    index=['Utah', 'Ohio', 'Texas', 'Oregon'])
frame
# Valor absoluto
np.abs(frame)
# APLICANDO FUNCIONES DE MAPEO
f = lambda x: x.max() - x.min() # Rango de valores
frame.apply(f) # Retorna Series
# Aplicar función a lo largo de columnas (se aplica por defecto a las filas)
frame.apply(f, axis='columns') # Útil para Series de Tiempo
# Podemos retornar DataFrames también
def f(x):
    return pd.Series([x.min(), x.max()], index=['min', 'max'])
frame.apply(f) # Retorna DataFrame<jupyter_output><empty_output><jupyter_text>Operaciones<jupyter_code>## ALINEAMIENTO AUTOMÁTICO
# ¿Cómo sabe Pandas cómo ordenar índices de dos objetos al sumarlos?
s1 = pd.Series([7.3, -2.5, 3.4, 1.5], index=['a', 'c', 'd', 'e'])
s2 = pd.Series([-2.1, 3.6, -1.5, 4, 3.1], index = ['a', 'c', 'e', 'f', 'g'])
s1
s2
# la f y g están después de e.
s1 + s2 # Hay valores NaN porque no hay índices comunes (d, f, g)
# DATAFRAMES
df1 = pd.DataFrame(np.arange(12.).reshape((3,4)),
                  columns=list('abcd'))
df2 = pd.DataFrame(np.arange(20.).reshape((4,5)),
                  columns=list('abcde'))
df2.iloc[1]['b'] = np.nan # Simulamos valores NaN
df1
df2
df1 + df2 # Muchos valores NaN
# Podemos solucionarlo con la opción fill_value
df1.add(df2, fill_value=0)<jupyter_output><empty_output><jupyter_text>En general, es una buena práctica utilizar funciones add, sub, div, etcétera en vez de operadores +, -, /, entre otros<jupyter_code># Las funcioens tienen variantes 'r' que intercambian argumentos
df1.sub(df2) # df1 - df2
df1.rsub(df2) # df2 - df1<jupyter_output><empty_output><jupyter_text>Operaciones entre Series y DataFrames: Broadcasting<jupyter_code>frame = pd.DataFrame(np.arange(12.).reshape((4,3)),
                    columns=list('bde'),
                    index=['Utah', 'Ohio', 'Texas', 'Oregon'])
series = frame.iloc[1]
series
frame
# ¿Como funciona una resta de un objetos 2D y 1D?
frame - series # ¡¡¡Broadcasting!!!!
# Broadcasting a lo largo de columnas (por defecto es por filas)
series3 = frame['d']
series3
frame
frame.sub(series3, axis='index') # o axis=0, es lo mismo<jupyter_output><empty_output><jupyter_text>Calculando Estadísticas<jupyter_code>df = pd.DataFrame([[1.4, np.nan], [7.1, -4.5],
                  [np.nan, np.nan], [0.75, -1.3]],
                 index=['a', 'b', 'c', 'd'],
                 columns=['one', 'two'])
df
# Sumas
df.sum()
df.sum(axis='columns')
# Valores mínimos y máximos
df.idxmax()
# Acumulaciones
df.cumsum()<jupyter_output><empty_output><jupyter_text>Descripción y más información<jupyter_code># describe arroja mucha información acerca de un DataFrame
df.describe()
# Descripción de valores no numéricos
obj = pd.Series(['a', 'a', 'b', 'c']*4)
obj
obj.describe()<jupyter_output><empty_output><jupyter_text>Valores únicos y Conteo<jupyter_code># ¿Qué valores se repiten?
obj = pd.Series(['c','a','d','a','a','b','b','c','c'])
uniques = obj.unique() # Ordenados según se avistaron por primera vez
uniques
# ¿Cuántas veces se repiten esos valores?
obj.value_counts() # Ordenados descendentemente
# Estas funciones igual existen como funciones globales de Pandas
pd.value_counts(obj, sort=False)
# UN PEQUEÑO EJEMPLO: ENCUESTA
# Dado una tabla que muestra las respuestas de diferentes personas...
data = pd.DataFrame({'Qu1': [1,3,4,3,4],
                    'Qu2': [2,3,1,2,3],
                    'Qu3': [1,5,2,4,4],})
data
# ...queremos saber cuántas dieron qué respuesta
result = data.apply(pd.value_counts).fillna(0)
result<jupyter_output><empty_output><jupyter_text>Correlación y Covarianza Nota: Instalar pandas_datareader usando la consola:  python3 -m pip install pandas-datareader<jupyter_code># Cargamos datos de Yahoo (stock prices and volumes)
import pandas_datareader.data as web
all_data = {ticker: web.get_data_yahoo(ticker)
           for ticker in ['AAPL', 'IBM', 'MSFT', 'GOOG']}
price = pd.DataFrame({ticker: data['Adj Close']
                     for ticker, data in all_data.items()})
volume = pd.DataFrame({ticker: data['Volume']
                      for ticker, data in all_data.items()})
# Calculamos el cambio porcentual de los precios (útil en Series de Tiempo)
returns = price.pct_change()
returns.tail()
# Correlación y covarianza (1 v 1)
returns['MSFT'].corr(returns['IBM'])
returns['MSFT'].cov(returns['IBM'])
# Matriz de correlación (Todos v Todos)
returns.corr()
# Matriz de covarianza
returns.cov()
# Correlation 1 v Todos
returns.corrwith(returns['IBM']) # Retorna Series
returns.corrwith(volume) # Retorna DataFrame<jupyter_output><empty_output><jupyter_text>Cargado y Guardado de Datos  CSV  Es un formato de guardado de información en texto plano donde los valores se encuentran separados por comas. Es muy frecuente encontrar datasets con este formato. Nota: En Linux, se utiliza el slash (/) para separar directorios. En Windows se utiliza el backslash (\\)<jupyter_code># Carguemos algo de datos
!cat ../examples/ex1.csv # Usa !type en Windows
df = pd.read_csv('../examples/ex1.csv')
df
# Podemos especificar el caracter que se usa para separar datos
pd.read_table('../examples/ex1.csv', sep=',')
# Archivos sin cabecera
!cat ../examples/ex2.csv
pd.read_csv('../examples/ex2.csv', header=None) # Las columnas serán números
# Especifica la cabecera
pd.read_csv('../examples/ex2.csv', names=['a','b','c','d','message'])
# Especifica tanto la cabecera como el índice
names = ['a','b','c','d','message']
pd.read_csv('../examples/ex2.csv', names=names, index_col='message')
# Datos separados por whitespaces
result = pd.read_table('../examples/ex3.txt', sep='\s+')
result # La primera columna es inferida como el Index
# MANEJANDO VALORES NAN
!cat ../examples/ex5.csv
result = pd.read_csv('../examples/ex5.csv')
# result
# Podemos usar una máscara booleana para reeamplzar los valores NaN
pd.isnull(result)
# GUARDANDO DATOS
data = pd.read_csv('../examples/ex5.csv')
data
# Usamos la funcion to_csv
data.to_csv('../examples/out.csv')
!cat ../examples/out.csv<jupyter_output>,something,a,b,c,d,message
0,one,1,2,3.0,4,
1,two,5,6,,8,world
2,three,9,10,11.0,12,foo
{'fraction_non_alphanumeric': 0.14834205933682373, 'fraction_numerical': 0.024869109947643978, 'mean_word_length': 4.636475409836065, 'pattern_counts': {'":': 0, '<': 66, '<?xml version=': 0, '>': 68, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '16', 'dir': 'jupyter-structured-clean-dedup', 'id': '42074', 'license': 'MIT', 'n_tokens_mistral': 5422, 'n_tokens_neox': 5202, 'n_words': 1473, 'path': 'Pandas/IntroToPandas.ipynb', 'repo_name': 'EstebanBrito/TallerIA-SISI2019', 'size': '13752'}