<jupyter_start><jupyter_text>Spike Challenge Octubre 2019 <NAME><jupyter_code>import pandas as pd
import numpy as np
from sklearn.neighbors import KNeighborsClassifier
from sklearn import metrics
from sklearn.model_selection import train_test_split
%matplotlib inline<jupyter_output><empty_output><jupyter_text>Importadas las librerías a utilizar, comenzamos:  Número 1Descarga del archivo csv para crear el dataframe.<jupyter_code>#df = pd.read_gbq('SELECT * FROM public.caudal_extra_min',project_id='spikelab') 
## Para base de datos en BigQuery (no tenía acceso)

df = pd.read_csv('caudal_extra.csv')<jupyter_output><empty_output><jupyter_text>Número 2 Hay varias filas con datos faltantes, porque no todas las cuencas tienen estaciones de medición de precipitación o temperatura.<jupyter_code>num_rows_nan_t = df['temp_max_promedio'].isna().sum()
num_rows_nan_p = df['precip_promedio'].isna().sum()
print('Hay ',num_rows_nan_t,' filas sin registro de temperatura')
print('Hay ',num_rows_nan_p,' filas sin registro de precipitación')<jupyter_output><empty_output><jupyter_text>Podemos ver un resumen de las tres principales variables<jupyter_code>print(df[['caudal','temp_max_promedio','precip_promedio']].describe())<jupyter_output><empty_output><jupyter_text>También podemos ver si la distribución de las variables es asimétrica<jupyter_code>print(df[['caudal','temp_max_promedio','precip_promedio']].skew())<jupyter_output><empty_output><jupyter_text>Las mediciones de caudal y precipitación se concentran más a la derecha (mediciones de caudal mayores al promedio) y las de temperatura a la izquierda (menores al promedio), considerando una distribución normal.Hay dos columnas iguales, gauge_id y codigo_estacion, por lo que se elimina una<jupyter_code>print(set(df['gauge_id'] == df['codigo_estacion']))
df.drop(columns='gauge_id',inplace=True)<jupyter_output><empty_output><jupyter_text>Hay columnas inútiles, como institución y fuente, porque tienen el mismo valor en todas las filas (mediciones)<jupyter_code>print('Hay ',len(set(df['institucion'])),' valor para todas las filas de la columna institución')
print('Hay ',len(set(df['fuente'])),' valor para todas las filas de la columna fuente')
df.drop(columns=['institucion','fuente'],inplace=True)<jupyter_output><empty_output><jupyter_text>También se puede saber cuantas cuencas y estaciones hay<jupyter_code>num_cuencas = len(set(df['codigo_cuenca']))
num_estaciones = len(set(df['codigo_estacion']))
print('Hay ',num_cuencas,' cuencas, monitoreadas en ',num_estaciones,' estaciones')<jupyter_output><empty_output><jupyter_text>Finalmente, dejamos solo las variables numéricas, transformamos la columna fecha a formato datetime y eliminamos el dataframe original<jupyter_code>dff = df[['codigo_estacion',
          'codigo_cuenca',
          'cantidad_observaciones',
          'altura',
          'latitud',
          'longitud',
          'fecha',
          'caudal',
          'precip_promedio',
          'temp_max_promedio']]
dff = df
dff['fecha'] = pd.to_datetime(dff['fecha'])

del df<jupyter_output><empty_output><jupyter_text>Número 3  a) Las estaciones, mientras existen, tienen registros de caudal en todo el periodo; para la temperatura y la precipitación no es el caso.<jupyter_code>def time_plot_una_estacion(codigo_estacion,columna,fecha_min,fecha_max):
    df1 = dff[dff['codigo_estacion'] == codigo_estacion]
    df1.set_index('fecha',inplace=True)
    df1 = df1[[columna]]
    df1 = df1[fecha_min:fecha_max]
    return df1.plot(title = 'Código estación: ' + str(codigo_estacion))<jupyter_output><empty_output><jupyter_text>A continuación hay un ejemplo de la implementación de la función.<jupyter_code>time_plot_una_estacion(4540001,'caudal','1965-01-06','1976-02-13')<jupyter_output><empty_output><jupyter_text>b)<jupyter_code>def time_plot_estaciones_varias_columnas(codigo_estacion,columnas,fecha_min,fecha_max):
    df1 = dff[dff['codigo_estacion'] == codigo_estacion]
    df1.set_index('fecha',inplace=True)
    df1 = df1[columnas]
    df1 = 100*(df1 - df1.min())/(df1.max()- df1.min())
    df1 = df1[fecha_min:fecha_max]
    return df1.plot(title= 'est: ' + str(codigo_estacion) + ', normalizado con min/max')<jupyter_output><empty_output><jupyter_text>Un ejemplo de la implementación:<jupyter_code>time_plot_estaciones_varias_columnas(4540001,['caudal','precip_promedio','temp_max_promedio'],'1980-01-06','1985-02-13')<jupyter_output><empty_output><jupyter_text>A modo de "prueba" se puede ver como la temperatura varía al pasar el año, lo que indica que parece estar bien.  Número 4 Utilizando una muy buena forma de transformar el formato datetime a meteorological season (verano,invierno,primavera y otoño), adaptada desde [aquí](https://stackoverflow.com/questions/44124436/python-datetime-to-season)<jupyter_code>dff['season'] = [(dt.month%12 + 3)//3 for dt in dff['fecha']]<jupyter_output><empty_output><jupyter_text>Se crea una función para calcular el percentil 95<jupyter_code>def per95(x):
    return np.percentile(x,95)<jupyter_output><empty_output><jupyter_text>Se crea un nuevo dataframe con pivot_table, donde los índices sean codigo_estacion y season, y los valores el percentil 95 de las 3 variables  en estudio<jupyter_code>df_q = pd.pivot_table(dff,index=['codigo_estacion','season'],values=['caudal','precip_promedio','temp_max_promedio'],\
                      aggfunc=per95)<jupyter_output><empty_output><jupyter_text>Se crea una función para asignar 1 si un número es mayor, y 0 si es menor a otro<jupyter_code>def mm(x,y):
    if (x >= y):
        return 1
    else:
        return 0<jupyter_output><empty_output><jupyter_text>Ahora, se aplica la función anterior comparando el dataframe df_q y dff, creando las columnas requeridas, para luego eliminar df_q y los arrays temporales. Este toma 5 minutos en mi computador<jupyter_code>c_xtrem = []
p_xtrem = []
t_xtrem = []
for dc,dp,dt,de,ds in zip(dff['caudal'],dff['precip_promedio'],\
                          dff['temp_max_promedio'],\
                          dff['codigo_estacion'],dff['season']):
    xtrem = df_q.loc[(de,ds)]
    c_xtrem.append(mm(dc,xtrem[0]))
    p_xtrem.append(mm(dp,xtrem[1]))
    t_xtrem.append(mm(dt,xtrem[2]))


dff['caudal_extremo'] = c_xtrem
dff['precip_extremo'] = p_xtrem
dff['temp_extremo'] = t_xtrem
del df_q, c_xtrem, p_xtrem, t_xtrem
dff[['codigo_estacion','season','caudal_extremo','precip_extremo','temp_extremo']].head()<jupyter_output><empty_output><jupyter_text>con 1:verano 2:otoño 3:invierno y 4:primavera El filtro por el percentil 95 es una buena manera de capturar los eventos extremos. Otra forma sería aprovechar la aleatoriedad de los datos; se me ocurre que la distribución de eventos extremos en el tiempo tiene más entropía, pero por ahora no sé como calcular algo así.  Número 5 Primero, se crea un nuevo dataframe, y utilizamos nuevamente pivot_tablet para hacer el aggregate de eventos extremos de caudal por cuenca<jupyter_code>df_caudal = dff.pivot_table(index='codigo_cuenca',values='caudal_extremo',aggfunc=np.sum)<jupyter_output><empty_output><jupyter_text>Luego, se grafica el número de ocurrencias de eventos extremos de caudal por cuenca<jupyter_code>df_caudal.plot.bar(title='N° de ocurrencias por cuenca')<jupyter_output><empty_output><jupyter_text>Podemos ver como la cantidad de eventos extremos de caudal entre algunas cuencas difiere en 1 orden de magnitud (10 veces más grandes) en algunos casos.  Número 6 Se crea una nueva columna, 'year', y un nuevo dataframe, df6<jupyter_code>years = [dt.year for dt in dff['fecha']]
dff['year'] = years
df6 = dff.drop(columns='fecha')<jupyter_output><empty_output><jupyter_text>Por última vez, se utiliza pivot_table para efectuar el aggregate de las variables, esta vez con la siguiente función, que entrega el porcentaje de eventos extremos de cada variable en cada año<jupyter_code>def perc100(x):
    return 100*np.mean(x)
df6 = pd.pivot_table(dff,index='year',values=['caudal_extremo','precip_extremo','temp_extremo'],aggfunc=perc100)<jupyter_output><empty_output><jupyter_text>Finalmente, se grafica y se eliminan las variables temporales<jupyter_code>df6.plot(title = 'Eventos extremos por año') 
del years, df6<jupyter_output><empty_output><jupyter_text>Solo observando el gráfico anterior, los eventos de precipitación extrema han disminuido, los de caudal no parecen mostrar una tendencia y los de temperatura parecen subir. Considerar por separado cuencas, zonas geográficas u otras funciones de aggregate quizás muestren mas claramente alguna tendencia.  Número 7 Se crean las variables 'month' y 'day', y se eliminan las filas que contengan valores missing<jupyter_code>dff['month'] = [dt.month for dt in dff['fecha']]
dff['day'] = [dt.day for dt in dff['fecha']]
dff.dropna(inplace=True)<jupyter_output><empty_output><jupyter_text>Se fija el tamaño del dataset de prueba en un 25%<jupyter_code>test_s = 0.25<jupyter_output><empty_output><jupyter_text>Se divide el dataset en features(datax) y objetivo(datay)<jupyter_code>datax = dff[['codigo_estacion',
          'codigo_cuenca',
          'cantidad_observaciones',
          'altura',
          'latitud',
          'longitud',
          'year',
          'month',
          'day',
          'caudal',   
          'precip_promedio',
          'temp_max_promedio']]


datay = dff[['caudal_extremo']]<jupyter_output><empty_output><jupyter_text>Se divide entre los train y test dataset<jupyter_code>x_train,x_test,y_train,y_test = train_test_split(datax,datay,test_size=test_s)<jupyter_output><empty_output><jupyter_text>Se entrenará un modelo K-Nearest Neighbors (KNN), con un radio de k=15, y ponderado por la distancia. El entrenamiento me tomó alrededor de 2 minutos<jupyter_code>knn = KNeighborsClassifier(n_neighbors=15,weights='distance')
knn.fit(x_train,y_train.values.ravel())
y_pred = knn.predict(x_test)<jupyter_output><empty_output><jupyter_text>Intenté ocupar todos los datos disponibles originales. Propongo utilizar las 11 variables para predecir caudal extremo en el futuro, o sea, simplemente ingresar los 11 valores requeridos del día a pronosticar. Como lo entiendo, más que predecir condiciones futuras, el modelo aprende los patrones cíclicos y la tendencia creciente o decreciente de la variable caudal extremo. KNN funciona en base a distancias euclidianas, por lo que en principio no hay restricciones numéricas para las features. Debería funcionar bien con datos razonables y dentro de las variaciones convencionales (no utilizar 60°C en temperatura, por ejemplo).  Número 8  a) Utilizando la métrica más sencilla, que es la comparación entre los datos predichos y el dataset de prueba apartado en el punto anterior, se tiene que:<jupyter_code>print('El modelo tiene una performance de ', round(metrics.accuracy_score(y_test, y_pred)*100,4),'%')<jupyter_output><empty_output><jupyter_text>Para KNN no conozco una manera directa de hacer análisis de sensibilidad, pero hay variables que quizás esten entregando la misma información (redundantes), como la cuenca, estación y las coordenadas.Quizás sirve "apagar" una de las features, y entrenar el modelo para ver como afecta en la metrica utilizada para medir performance. A pesar de lo anterior, el entrenamiento no tarda mucho, y parece entregar buenos resultados. Es un buen modelo para comenzar.  b) No comprendo la pregunta; en los modelos que conozco (y este en particular) creo que no se puede elegir exactamente qué datos se utilizaran para entrenar el modelo. Si la pregunta consiste en ocupar el 70% del dataset para entrenar, entonces<jupyter_code>test_s = 0.3
x_train,x_test,y_train,y_test = train_test_split(datax,datay,test_size=test_s)

knn1 = KNeighborsClassifier(n_neighbors=15,weights='distance')
knn1.fit(x_train,y_train.values.ravel())
y_pred = knn1.predict(x_test)
print('El modelo tiene una performance de ', round(metrics.accuracy_score(y_test, y_pred)*100,4),'%')<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10005027652086476, 'fraction_numerical': 0.013993631640690464, 'mean_word_length': 6.331081081081081, 'pattern_counts': {'":': 0, '<': 122, '<?xml version=': 0, '>': 123, 'https://': 1, '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': '30', 'dir': 'jupyter-structured-clean-dedup', 'id': '395129', 'license': 'MIT', 'n_tokens_mistral': 4419, 'n_tokens_neox': 4280, 'n_words': 1073, 'path': 'Spike Challenge.ipynb', 'repo_name': 'IvanJV/SC2019', 'size': '11944'}