QUIMAD / statistical_analysis.py
metamatematico's picture
Upload statistical_analysis.py with huggingface_hub
5098d69 verified
"""
Análisis estadístico de los resultados de QUIMAD.
Carga experiment_results.csv y produce:
1. Tabla media ± desviación estándar por función y optimizador
2. Test de Wilcoxon (QUIMAD vs cada baseline, por función)
3. Rankings con mediana y mejor valor
4. Tabla exportable a Markdown y LaTeX
Uso:
python statistical_analysis.py
python statistical_analysis.py --csv results/experiment_results.csv
"""
import argparse
from pathlib import Path
import numpy as np
import pandas as pd
from scipy import stats
# ── Carga ────────────────────────────────────────────────────────────────────
def load(csv_path: str) -> pd.DataFrame:
df = pd.read_csv(csv_path)
# Para cada run, tomamos el mejor valor alcanzado en esa corrida
best_per_run = (
df.groupby(['function', 'optimizer', 'run'])['best_global_objective']
.min()
.reset_index()
.rename(columns={'best_global_objective': 'best'})
)
return best_per_run
# ── Estadísticas descriptivas ────────────────────────────────────────────────
def descriptive_table(df: pd.DataFrame) -> pd.DataFrame:
"""Media, desviación estándar, mediana y mejor valor por función/optimizador."""
stats_df = (
df.groupby(['function', 'optimizer'])['best']
.agg(
runs='count',
mean='mean',
std='std',
median='median',
best='min',
worst='max',
)
.round(4)
.reset_index()
)
# Columna combinada mean ± std para tablas
stats_df['mean±std'] = (
stats_df['mean'].map('{:.4f}'.format)
+ ' ± '
+ stats_df['std'].map('{:.4f}'.format)
)
return stats_df
# ── Test de Wilcoxon ─────────────────────────────────────────────────────────
ALPHA = 0.05 # nivel de significancia
def wilcoxon_vs_quimad(df: pd.DataFrame) -> pd.DataFrame:
"""
Para cada (función, baseline), aplica el test de Wilcoxon de rangos con signo
entre QUIMAD y el baseline.
H₀: las medianas son iguales.
H₁: QUIMAD tiene una mediana distinta (test bilateral).
Reporta también la dirección: QUIMAD < baseline → QUIMAD gana.
"""
results = []
for func in df['function'].unique():
q_vals = df[(df['function'] == func) & (df['optimizer'] == 'QIMAD')]['best'].values
for opt in df['optimizer'].unique():
if opt == 'QIMAD':
continue
b_vals = df[(df['function'] == func) & (df['optimizer'] == opt)]['best'].values
# Alinear longitudes (por si difieren)
n = min(len(q_vals), len(b_vals))
if n < 5:
results.append(dict(function=func, vs=opt, p_value=np.nan,
significant='—', winner='N/A (pocas muestras)'))
continue
try:
stat, p = stats.wilcoxon(q_vals[:n], b_vals[:n], alternative='two-sided')
except ValueError:
# Diferencias todas cero (caso degenerado)
results.append(dict(function=func, vs=opt, p_value=1.0,
significant='No', winner='Empate'))
continue
significant = 'Sí ✓' if p < ALPHA else 'No'
if p < ALPHA:
winner = 'QUIMAD' if np.median(q_vals) < np.median(b_vals) else opt
else:
winner = 'Sin diferencia significativa'
results.append(dict(
function=func,
vs=opt,
p_value=round(p, 5),
significant=significant,
winner=winner,
))
return pd.DataFrame(results)
# ── Ranking global ────────────────────────────────────────────────────────────
def ranking_table(df: pd.DataFrame) -> pd.DataFrame:
"""Número de funciones en que cada optimizador es significativamente mejor."""
median_df = (
df.groupby(['function', 'optimizer'])['best']
.median()
.reset_index()
)
ranks = []
for func in median_df['function'].unique():
sub = median_df[median_df['function'] == func].sort_values('best')
for rank, (_, row) in enumerate(sub.iterrows(), 1):
ranks.append({'function': func, 'optimizer': row['optimizer'], 'rank': rank})
rank_df = pd.DataFrame(ranks)
summary = (
rank_df.groupby('optimizer')['rank']
.agg(rank1=lambda x: (x == 1).sum(),
mean_rank='mean')
.reset_index()
.sort_values('rank1', ascending=False)
)
return summary
# ── Exportar Markdown ────────────────────────────────────────────────────────
def to_markdown_table(desc: pd.DataFrame) -> str:
"""Tabla media ± std en formato Markdown, pivoteada por optimizador."""
pivot = desc.pivot(index='function', columns='optimizer', values='mean±std')
# Marcar el mejor (menor media) en cada fila
medias = desc.pivot(index='function', columns='optimizer', values='mean')
lines = []
cols = pivot.columns.tolist()
header = '| Función | ' + ' | '.join(cols) + ' |'
sep = '|---|' + '---|' * len(cols)
lines.append(header)
lines.append(sep)
for func in pivot.index:
best_opt = medias.loc[func].idxmin()
row_parts = []
for col in cols:
val = pivot.loc[func, col]
row_parts.append(f'**{val}**' if col == best_opt else val)
lines.append('| ' + func + ' | ' + ' | '.join(row_parts) + ' |')
return '\n'.join(lines)
def to_latex_table(desc: pd.DataFrame) -> str:
"""Tabla en formato LaTeX lista para paper."""
pivot = desc.pivot(index='function', columns='optimizer', values='mean±std')
medias = desc.pivot(index='function', columns='optimizer', values='mean')
cols = pivot.columns.tolist()
lines = [
r'\begin{table}[h]',
r'\centering',
r'\caption{QUIMAD vs baselines: media $\pm$ desviación estándar (30 corridas, D=10)}',
r'\begin{tabular}{l' + 'c' * len(cols) + '}',
r'\hline',
'Función & ' + ' & '.join(cols) + r' \\',
r'\hline',
]
for func in pivot.index:
best_opt = medias.loc[func].idxmin()
parts = []
for col in cols:
val = pivot.loc[func, col].replace('±', r'$\pm$')
parts.append(r'\textbf{' + val + '}' if col == best_opt else val)
lines.append(func + ' & ' + ' & '.join(parts) + r' \\')
lines += [r'\hline', r'\end{tabular}', r'\end{table}']
return '\n'.join(lines)
# ── Main ─────────────────────────────────────────────────────────────────────
def main(csv_path='results/experiment_results.csv'):
print(f'\nCargando: {csv_path}')
df = load(csv_path)
n_runs = df['run'].nunique()
opts = df['optimizer'].unique().tolist()
funcs = df['function'].unique().tolist()
print(f'Corridas por experimento : {n_runs}')
print(f'Optimizadores : {opts}')
print(f'Funciones : {funcs}\n')
# ── 1. Estadísticas descriptivas ─────────────────────────────────────────
desc = descriptive_table(df)
print('=' * 70)
print('TABLA 1 — Media ± Desviación estándar (mejor valor por corrida)')
print('=' * 70)
pivot_display = desc.pivot(index='function', columns='optimizer',
values='mean±std')
print(pivot_display.to_string())
# ── 2. Test de Wilcoxon ──────────────────────────────────────────────────
wdf = wilcoxon_vs_quimad(df)
print('\n' + '=' * 70)
print(f'TABLA 2 — Test de Wilcoxon: QUIMAD vs baselines (alpha = {ALPHA})')
print('=' * 70)
print(wdf.to_string(index=False))
# ── 3. Ranking ───────────────────────────────────────────────────────────
rank = ranking_table(df)
print('\n' + '=' * 70)
print('TABLA 3 — Ranking global (veces en 1er lugar por mediana)')
print('=' * 70)
print(rank.to_string(index=False))
# ── 4. Exportar ──────────────────────────────────────────────────────────
md_table = to_markdown_table(desc)
latex_table = to_latex_table(desc)
Path('results').mkdir(exist_ok=True)
with open('results/stats_markdown.md', 'w', encoding='utf-8') as f:
f.write('# Resultados estadísticos QUIMAD\n\n')
f.write(f'> {n_runs} corridas independientes, semillas {42}{42+n_runs-1}, D=10\n\n')
f.write('## Media ± desviación estándar\n\n')
f.write(md_table + '\n\n')
f.write('## Test de Wilcoxon (QUIMAD vs cada baseline)\n\n')
# to_markdown requiere tabulate; usamos to_string como fallback
try:
f.write(wdf.to_markdown(index=False) + '\n')
except ImportError:
f.write(wdf.to_string(index=False) + '\n')
with open('results/stats_latex.tex', 'w', encoding='utf-8') as f:
f.write(latex_table)
print('\nArchivos generados:')
print(' results/stats_markdown.md')
print(' results/stats_latex.tex')
return desc, wdf, rank
if __name__ == '__main__':
ap = argparse.ArgumentParser()
ap.add_argument('--csv', default='results/experiment_results.csv')
args = ap.parse_args()
main(args.csv)