diff --git a/earthquakes.py b/earthquakes.py index a122bb5..4acf493 100644 --- a/earthquakes.py +++ b/earthquakes.py @@ -8,11 +8,11 @@ from datetime import datetime import pandas as pd -from utils import parser, crud, stats, utils +from utils import parser, crud, stats, utils, visuals, filters HEADER = """=== Terramotos ===""" -EVENT_COLS = ["Data", "Latitude", "Longitude", "Profundidade", "Tipo Evento", "Gap", "Magnitudes", "Regiao", "Sentido"] +EVENT_COLS = ["Data", "Latitude", "Longitude", "Profundidade", "Tipo Evento", "Gap", "Magnitudes", "Regiao", "Sentido", "Pub", "SZ", "VZ"] STATION_COLS = ["Estacao", "Hora", "Min", "Seg", "Componente", "Distancia Epicentro", "Tipo Onda"] MENU ="""[1] Criar a base de dados @@ -23,6 +23,8 @@ MENU ="""[1] Criar a base de dados [7] Guardar como CSV [8] Estatísticas [9] Criar uma entrada +[10] Gráficos +[11] Filtros (T7) [Q] Sair """ @@ -51,6 +53,7 @@ def guardar_csv(df: pd.DataFrame, fname: str): def main(): isRunning = True db = None + original_db = None retInfo = None @@ -67,9 +70,11 @@ def main(): if _file_exists(fname) and fname.endswith(".json"): db = pd.read_json(fname) + original_db = db.copy() print("Base de dados populada.") elif _file_exists(fname): db = parser.parse(fname) + original_db = db.copy() input("Base de dados populada. Enter para voltar ao menu inicial") else: input("Base de dados não encontrada. Por favor tenta de novo.") @@ -180,6 +185,21 @@ def main(): input() else: retInfo = "Base de dados não encontrada!" + + case "10": + if db is not None: + visuals.visual_menu(db) + else: + retInfo = "Base de dados não encontrada!" + + case "11": + if db is not None: + # Passa db e original_db para o menu de filtros + # Retorna a nova db ativa (filtrada ou redefinida) + db = filters.filter_menu(db, original_db) + else: + retInfo = "Base de dados não encontrada!" + case "q": isRunning = False continue @@ -215,8 +235,8 @@ def _prettify_event(df): stations = df[["Estacao", "Componente", "Tipo Onda", "Amplitude"]] info = df.drop_duplicates(subset="Data", keep="first") data = datetime.fromisoformat(info.Data.values[0]).strftime("%c") - print(f"Região: {info["Regiao"].values[0]}\nData: {data}\nLatitude: {info.Lat.values[0]}\nLongitude: {info.Long.values[0]}" - + f"\nProfundidade: {info.Prof.values[0]}\nTipo de evento: {info['Tipo Ev'].values[0]}\n") + print(f"Região: {info["Regiao"].values[0]}\nData: {data}\nLatitude: {info['Latitude'].values[0]}\nLongitude: {info['Longitude'].values[0]}" + + f"\nProfundidade: {info['Profundidade'].values[0]}\nTipo de evento: {info['Tipo Evento'].values[0]}\n") if __name__ == '__main__': main() diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..e58eb5d --- /dev/null +++ b/shell.nix @@ -0,0 +1,14 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + buildInputs = with pkgs; [ + python3 + python3Packages.pandas + python3Packages.numpy + python3Packages.matplotlib + ]; + + shellHook = '' + echo "Funcionou" + ''; +} diff --git a/utils/crud.py b/utils/crud.py index 338c053..e8b0ffb 100644 --- a/utils/crud.py +++ b/utils/crud.py @@ -8,7 +8,7 @@ pd.set_option('display.width', 150) # -- globals -HEADER_COLS = ["Data", "Distancia", "Tipo Ev", "Lat", "Long", "Prof", "Magnitudes"] +HEADER_COLS = ["Data", "Distancia", "Tipo Evento", "Latitude", "Longitude", "Profundidade", "Magnitudes"] TABLE_READ_RET = ["Estacao", "Hora", "Min", "Seg", "Componente", "Amplitude"] # -- helper funcs @@ -33,14 +33,13 @@ def get_unique_events_table(df): def read_header(df, event_id): - # Informações do header do evento + # Obtém a informação da primeira linha do evento (cabeçalho) row = df[df["ID"] == event_id].iloc[0] cols = list(df.columns) - # end = cols.index("ID") - 1 - # header_cols = cols[:end] - # Para selecionar todas as colunas em vez de só algumas + info = [] for (i, col) in enumerate(HEADER_COLS): + # Constrói a string formatada "Índice Nome: Valor" info.append(f"{i+1} {col}: {row[col]}") infoString = f"Header do evento {event_id}:\n" + "\n".join(info) return infoString @@ -56,15 +55,23 @@ def get_table(df, event_id): def read_table_row(df, event_id, row_number_1): - # retorna uma linha específica da tabela + # Retorna uma linha específica da tabela de estações + # row_number_1 é o índice dado pelo utilizador (começa em 1) + # row_number_0 é o índice real da lista (começa em 0) row_number_0 = row_number_1 - 1 table = get_table(df, event_id) + + # Verifica se a linha pedida existe dentro das linhas deste evento if row_number_0 < 0 or row_number_0 >= len(table): return f"Linha {row_number_1} não pertence ao evento {event_id}." + row = table.iloc[row_number_0] cols = list(df.columns) + + # Encontra onde começam as colunas da estação para mostrar apenas os dados relevantes start = cols.index("Estacao") tableCols = cols[start:] + info = [] for (i, col) in enumerate(tableCols): info.append(f"{i+1} {col}: {row[col]}") @@ -79,9 +86,10 @@ def update_table_row(df, row_line, new_data): def update_header(df, event_id, new_data): - # atualiza o header de um evento + # Atualiza o cabeçalho de um evento com os novos dados for key, value in new_data.items(): if key in df.columns: + # Atualiza todas as linhas deste evento (ID == event_id) com o novo valor df.loc[(df["ID"] == event_id) | df.iloc[0], key] = value return f"Header do evento {event_id} atualizado com sucesso." @@ -94,48 +102,60 @@ def delete_event(df, event_id): def delete_table_row(df, event_id, row_number): - # Apaga uma linha específica da tabela do evento - # Cria uma nova linha vazia no dataframe na posição insertion_point + # Apaga uma linha específica da tabela de estações de um evento + + # Encontra todos os índices (números de linha no DataFrame que pertencem a este evento matching_indices = df.index[df['ID'] == event_id].tolist() first_event_row = matching_indices[0] last_event_row = matching_indices[-1] + # Garante que não estamos a apagar uma linha que pertence a outro evento if row_number < first_event_row or row_number > last_event_row: return df, f"Erro: A posição a apagar, {row_number} está fora do intervalo permitido para o evento {event_id}." + new_df = df.drop([row_number]).reset_index(drop=True) - return new_df, f"Linha {row_choice} apagada com sucesso!" + return new_df, f"Linha {row_number} apagada com sucesso!" def create_blank_event(df, event_id): - # Criar um evento vazio com linha de header e 1 linha de coluna + # Cria um novo evento vazio + # Primeiro, avança os IDs de todos os eventos seguintes para arranjar espaço df.loc[df["ID"] >= event_id, "ID"] += 1 + # Cria 2 linhas novas: uma para o cabeçalho e outra vazia para dados blank_row_df = pd.DataFrame(columns=df.columns, index=[0, 1]) blank_row_df["ID"] = event_id blank_row_df = blank_row_df.astype(df.dtypes) + # Junta as novas linhas ao dataframe principal new_df = pd.concat([df, blank_row_df], ignore_index=True) + # Ordena por ID para garantir que fica tudo na ordem certa (mergesort é estável) new_df = new_df.sort_values(by="ID", kind="mergesort").reset_index(drop=True) return new_df def create_table_row(df, event_id, insertion_point): - # Cria uma nova linha vazia no dataframe na posição insertion_point + # Insere uma nova linha vazia numa posição específica dentro do evento + + # Encontra os limites (início e fim) do evento atual matching_indices = df.index[df['ID'] == event_id].tolist() first_event_row = matching_indices[0] last_event_row = matching_indices[-1] + # Valida se o ponto de inserção é válido para este evento if insertion_point < first_event_row or insertion_point > last_event_row + 1: return df, f"Erro: A posição de inserção {insertion_point} está fora do intervalo permitido para o evento {event_id}" + # Cria a nova linha new_row_df = pd.DataFrame(columns=df.columns, index=[0]) new_row_df['ID'] = event_id new_row_df = new_row_df.fillna(0) new_row_df = new_row_df.astype(df.dtypes) + # Parte o dataframe em dois (antes e depois do ponto de inserção) e mete a nova linha no meio df_before = df.iloc[:insertion_point] df_after = df.iloc[insertion_point:] diff --git a/utils/filters.py b/utils/filters.py new file mode 100644 index 0000000..b29fc88 --- /dev/null +++ b/utils/filters.py @@ -0,0 +1,108 @@ +import pandas as pd +from datetime import datetime +import os +import sys + + +def filter_by_date(df: pd.DataFrame, start_date: str, end_date: str) -> pd.DataFrame: + # filtra o dataframe por intervalo de datas (strings em formato ISO) + mask = (df['Data'] >= start_date) & (df['Data'] <= end_date) + return df.loc[mask] + +def filter_by_depth(df: pd.DataFrame, min_depth: float, max_depth: float) -> pd.DataFrame: + mask = (df['Profundidade'] >= min_depth) & (df['Profundidade'] <= max_depth) + return df.loc[mask] + +def filter_by_magnitude(df: pd.DataFrame, min_mag: float, max_mag: float, mag_type: str = 'L') -> pd.DataFrame: + def filter_mag(mags): + # Filtrar por tipo de magnitude específico + vals = [float(m['Magnitude']) for m in mags if m.get('Tipo') == mag_type] + if not vals: + return False + # Se houver múltiplas magnitudes do mesmo tipo, usa o máximo para filtragem + mx = max(vals) + return min_mag <= mx <= max_mag + + mask = df['Magnitudes'].apply(filter_mag) + return df.loc[mask] + +# -- t7 filters + +def filter_by_gap(df: pd.DataFrame, max_gap: float) -> pd.DataFrame: + # Filtra onde Gap <= max_gap + return df[df['Gap'] <= max_gap] + +def filter_by_quality(df: pd.DataFrame, quality: str) -> pd.DataFrame: + return df[df['Pub'] == quality] + +def filter_by_zone(df: pd.DataFrame, zone_type: str, zone_val: str) -> pd.DataFrame: + return df[df[zone_type] == zone_val] + +FILTER_MENU = """[1] Filtrar por Data (Inicio:Fim) +[2] Filtrar por Gap (< Valor) +[3] Filtrar por Qualidade (EPI) +[4] Filtrar por Zona SZ +[5] Filtrar por Zona VZ +[6] Filtrar por Magnitude (Min:Max) +[7] Filtrar por Profundidade (Min:Max) +[R] Reset Filtros + +[Q] Voltar +""" + +def filter_menu(db: pd.DataFrame, original_db: pd.DataFrame): + currDb = db + + while True: + os.system("cls" if sys.platform == "windows" else "clear") + print("=== T7: Filtros ===") + print(f"Linhas actuais: {len(currDb)}") + print(FILTER_MENU) + usrIn = input("Opção: ").lower() + + + match usrIn: + case "1": + start = input("Data Inicio (YYYY-MM-DD): ") + end = input("Data Fim (YYYY-MM-DD): ") + currDb = filter_by_date(currDb, start, end) + + case "2": + val = float(input("Gap Máximo: ")) + currDb = filter_by_gap(currDb, val) + + + case "3": + confirm = input("Filtrar apenas eventos com Qualidade EPI? (s/n): ").lower() + if confirm == 's': + currDb = filter_by_quality(currDb, "EPI") + else: + print("Filtro não aplicado.") + + case "4": + val = input("Zona SZ (ex: SZ31): ") + currDb = filter_by_zone(currDb, "SZ", val) + + case "5": + val = input("Zona VZ (ex: VZ14): ") + currDb = filter_by_zone(currDb, "VZ", val) + + case "6": + print("Filtrar por Magnitude Tipo 'L'") + min_m = float(input("Min Mag L: ")) + max_m = float(input("Max Mag L: ")) + + currDb = filter_by_magnitude(currDb, min_m, max_m, "L") + + case "7": + min_d = float(input("Min Profundidade: ")) + max_d = float(input("Max Profundidade: ")) + currDb = filter_by_depth(currDb, min_d, max_d) + + case "r": + currDb = original_db.copy() + + case "q": + return currDb + case _: + pass \ No newline at end of file diff --git a/utils/parser.py b/utils/parser.py index 1e80b15..1d1c4e7 100644 --- a/utils/parser.py +++ b/utils/parser.py @@ -1,4 +1,3 @@ -# pyright: basic import io from collections import defaultdict @@ -6,12 +5,12 @@ from datetime import datetime import pandas as pd -# --- globals --- +# --- variáveis globais --- DIST_IND = {"L": "Local", "R": "Regional", "D": "Distante"} TYPE = {"Q": "Quake", "V": "Volcanic", "U": "Unknown", "E": "Explosion"} -# --- helper funcs --- +# --- funções auxiliares --- def is_blank(l: str) -> bool: return len(l.strip(" ")) == 0 @@ -156,10 +155,22 @@ def _parse_mag(line: str): def _parse_type_3(data: list[str]): comments = {} for line in data: - if line.startswith(" SENTIDO") or line.startswith(" REGIAO"): + if line.startswith(" SENTIDO") or line.startswith(" REGIAO") or line.startswith(" PUB"): c, v = line[:-2].strip().split(": ", maxsplit=1) - v = v.split(",")[0] - comments[c.capitalize()] = v + + if c == "REGIAO": + parts = v.split(",") + comments["Regiao"] = parts[0].strip() + for p in parts[1:]: + p = p.strip() + if "SZ" in p: + comments["SZ"] = p + elif "VZ" in p: + comments["VZ"] = p + elif c == "PUB": + comments["Pub"] = v.strip() + else: + comments[c.capitalize()] = v.split(",")[0] return comments diff --git a/utils/stats.py b/utils/stats.py index 8ef25cd..390dfd3 100644 --- a/utils/stats.py +++ b/utils/stats.py @@ -1,5 +1,3 @@ -# pyright: basic - import os import sys @@ -16,6 +14,7 @@ STAT_MENU = """[1] Média [4] Máximo [5] Mínimo [6] Moda +[T] Estatísticas Temporais (T5) [Q] Voltar ao menu principal """ @@ -43,6 +42,120 @@ def filter_submenu(type: str): return None + +# -- t5 funcs + +def _get_unique_events(df: pd.DataFrame) -> pd.DataFrame: + return df.drop_duplicates(subset="ID", keep='first') + +def convert_to_datetime(df: pd.DataFrame) -> pd.DataFrame: + # Converte coluna Data para objetos datetime + df = df.copy() + df['Data'] = pd.to_datetime(df['Data'], format='mixed') + return df + +def events_per_period(df: pd.DataFrame, period: str): + # Calcula o número de eventos por dia ('D') ou mês ('M') + df = convert_to_datetime(df) + events = _get_unique_events(df) + + if period == 'M': + period = 'ME' + + res = events.set_index('Data').resample(period).size() + return res.index, res.values + +def stats_depth_month(df: pd.DataFrame): + # Calcula estatísticas de Profundidade por Mês + df = convert_to_datetime(df) + events = _get_unique_events(df) + + grouped = events.set_index('Data').resample('ME')['Profundidade'] + + stats_df = pd.DataFrame({ + 'Mean': grouped.mean(), + 'Std': grouped.std(), + 'Median': grouped.median(), + 'Q1': grouped.quantile(0.25), + 'Q3': grouped.quantile(0.75), + 'Min': grouped.min(), + 'Max': grouped.max() + }) + return stats_df + +def stats_mag_month(df: pd.DataFrame): + # Calcula estatísticas de Magnitude por Mês + df = convert_to_datetime(df) + events = _get_unique_events(df) + + def get_max_mag(mags): + vals = [float(m['Magnitude']) for m in mags if 'Magnitude' in m] + return max(vals) if vals else np.nan + + events = events.copy() + events['MaxMag'] = events['Magnitudes'].apply(get_max_mag) + + grouped = events.set_index('Data').resample('ME')['MaxMag'] + + stats_df = pd.DataFrame({ + 'Mean': grouped.mean(), + 'Std': grouped.std(), + 'Median': grouped.median(), + 'Q1': grouped.quantile(0.25), + 'Q3': grouped.quantile(0.75), + 'Min': grouped.min(), + 'Max': grouped.max() + }) + return stats_df + + +# -- t5 menu + +T5_MENU = """[1] Número de eventos por dia +[2] Número de eventos por mês +[3] Estatísticas Profundidade por mês +[4] Estatísticas Magnitude por mês + +[Q] Voltar +""" + +def t5_menu(df: pd.DataFrame): + while True: + os.system("cls" if sys.platform == "windows" else "clear") + print(STAT_HEADER + "\n" + " == T5: Estatísticas Temporais ==\n" + T5_MENU) + usrIn = input("Opção: ").lower() + + match usrIn: + case "1": + dates, counts = events_per_period(df, 'D') + print("\nEventos por Dia:") + print(pd.DataFrame({'Data': dates, 'Contagem': counts}).to_string(index=False)) + + case "2": + dates, counts = events_per_period(df, 'M') + print("\nEventos por Mês:") + print(pd.DataFrame({'Data': dates, 'Contagem': counts}).to_string(index=False)) + + case "3": + st = stats_depth_month(df) + print("\nEstatísticas Profundidade por Mês:") + print(st.to_string()) + + case "4": + st = stats_mag_month(df) + print("\nEstatísticas Magnitude por Mês:") + print(st.to_string()) + + case "q": + return + case _: + pass + + input("\n[Enter] para continuar...") + + +# -- stat menu + def stat_menu(df: pd.DataFrame): inStats = True while inStats: @@ -51,6 +164,10 @@ def stat_menu(df: pd.DataFrame): usrIn = input("Opção: ").lower() match usrIn: + case "t": + t5_menu(df) + continue + case "1": c = filter_submenu("Média") @@ -106,7 +223,7 @@ def stat_menu(df: pd.DataFrame): continue case "6": - c = filter_submenu("Mínimo") + c = filter_submenu("Moda") if c is not None: retValue = moda(df, c) @@ -120,11 +237,12 @@ def stat_menu(df: pd.DataFrame): case _: pass + input("Clica `Enter` para continuar") def average(df: pd.DataFrame, filter_by): - events = df.drop_duplicates(subset="ID", keep='first') + events = _get_unique_events(df) values = events[filter_by].to_numpy() if filter_by == "Magnitudes": @@ -136,7 +254,7 @@ def average(df: pd.DataFrame, filter_by): def variance(df, filter_by): - events = df.drop_duplicates(subset="ID", keep='first') + events = _get_unique_events(df) values = events[filter_by].to_numpy() if filter_by == "Magnitudes": @@ -149,7 +267,7 @@ def variance(df, filter_by): def std_dev(df, filter_by): - events = df.drop_duplicates(subset="ID", keep='first') + events = _get_unique_events(df) values = events[filter_by].to_numpy() if filter_by == "Magnitudes": @@ -162,7 +280,7 @@ def std_dev(df, filter_by): def max_v(df, filter_by): - events = df.drop_duplicates(subset="ID", keep='first') + events = _get_unique_events(df) values = events[filter_by].to_numpy() if filter_by == "Magnitudes": @@ -172,7 +290,7 @@ def max_v(df, filter_by): def min_v(df, filter_by): - events = df.drop_duplicates(subset="ID", keep='first') + events = _get_unique_events(df) values = events[filter_by].to_numpy() if filter_by == "Magnitudes": @@ -182,7 +300,7 @@ def min_v(df, filter_by): def moda(df, filter_by): - events = df.drop_duplicates(subset="ID", keep='first') + events = _get_unique_events(df) values = events[filter_by].to_numpy() if filter_by == "Magnitudes": diff --git a/utils/visuals.py b/utils/visuals.py new file mode 100644 index 0000000..b88f644 --- /dev/null +++ b/utils/visuals.py @@ -0,0 +1,133 @@ +import matplotlib.pyplot as plt +import pandas as pd +import sys +import os +import numpy as np + +from utils import stats + +# -- helpers + +def plot_bar(x, y, xLabel, yLabel, title): + plt.figure(figsize=(10, 6)) + plt.bar(x, y) + plt.xlabel(xLabel) + plt.ylabel(yLabel) + plt.title(title) + plt.xticks(rotation=45) + plt.tight_layout() + plt.show() + +def plot_linear_with_std(x, mean, std, xLabel, yLabel, title): + plt.figure(figsize=(10, 6)) + plt.errorbar(x, mean, yerr=std, fmt='-o', capsize=5, ecolor='red') + plt.xlabel(xLabel) + plt.ylabel(yLabel) + plt.title(title) + plt.xticks(rotation=45) + plt.grid(True) + plt.tight_layout() + plt.show() + +def plot_boxplot(dataList, labels, xLabel, yLabel, title): + # dataList: lista de arrays/series, um para cada etiqueta + # labels: lista de etiquetas correspondentes a dataList + plt.figure(figsize=(10, 6)) + plt.boxplot(dataList, labels=labels) + plt.xlabel(xLabel) + plt.ylabel(yLabel) + plt.title(title) + plt.xticks(rotation=90) + plt.tight_layout() + plt.show() + +# -- t6 logic + +def viz_events_per_period(df: pd.DataFrame, period: str, title_suffix: str): + dates, counts = stats.events_per_period(df, period) + # Formatar datas para melhor leitura no gráfico + if period == 'D': + # dates é um DatetimeIndex + labels = [d.strftime('%Y-%m-%d') for d in dates] + else: + labels = [d.strftime('%Y-%m') for d in dates] + + plot_bar(labels, counts, "Data", "Número de Eventos", f"Eventos por {title_suffix}") + +def viz_linear_stats(df: pd.DataFrame, target: str): + # Média +/- Desvio Padrão + if target == 'Profundidade': + st = stats.stats_depth_month(df) + unit = "km" + else: # Magnitude + st = stats.stats_mag_month(df) + unit = "Magn" + + labels = [d.strftime('%Y-%m') for d in st.index] + + plot_linear_with_std(labels, st['Mean'], st['Std'], "Mês", f"{target} ({unit})", f"Média e Desvio Padrão de {target} por Mês") + +def viz_boxplot(df: pd.DataFrame, target: str): + df = stats.convert_to_datetime(df) + events = stats._get_unique_events(df) + + # Agrupar por mês + grouped = events.set_index('Data').resample('ME') + + data_to_plot = [] + labels = [] + + for name, group in grouped: + if target == 'Profundidade': + vals = group['Profundidade'].dropna().values + else: + # Extrair magnitudes máximas + def get_max_mag(mags): + vals = [float(m['Magnitude']) for m in mags if 'Magnitude' in m] + return max(vals) if vals else np.nan + vals = group['Magnitudes'].apply(get_max_mag).dropna().values + + if len(vals) > 0: + data_to_plot.append(vals) + labels.append(name.strftime('%Y-%m')) + + plot_boxplot(data_to_plot, labels, "Mês", target, f"Boxplot de {target} por Mês") + + +# --- Menu --- + +VISUALS_MENU = """[1] Gráfico Barras: Eventos por Dia +[2] Gráfico Barras: Eventos por Mês +[3] Gráfico Linear: Profundidade (Média +/- DP) por Mês +[4] Gráfico Linear: Magnitude (Média +/- DP) por Mês +[5] Boxplot: Profundidade por Mês +[6] Boxplot: Magnitude por Mês + +[Q] Voltar +""" + +HEADER = "=== T6: Representação Gráfica ===" + +def visual_menu(df: pd.DataFrame): + while True: + os.system("cls" if sys.platform == "windows" else "clear") + print(HEADER + "\n" + VISUALS_MENU) + usrIn = input("Opção: ").lower() + + match usrIn: + case "1": + viz_events_per_period(df, 'D', "Dia") + case "2": + viz_events_per_period(df, 'M', "Mês") + case "3": + viz_linear_stats(df, "Profundidade") + case "4": + viz_linear_stats(df, "Magnitude") + case "5": + viz_boxplot(df, "Profundidade") + case "6": + viz_boxplot(df, "Magnitude") + case "q": + return + case _: + pass