TS_Analysis / app.py
carperca23's picture
arreglo 2
2c9b81b
Raw
History Blame Contribute Delete
30.2 kB
#!/usr/bin/env python3
import json
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
from scipy import stats
from scipy.signal import find_peaks
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import DBSCAN
import gradio as gr
# Store global para mantener los datos analizados
analysis_store = {}
class TrendsAnalyzer:
def __init__(self):
self.scaler = StandardScaler()
def detect_trends(self, data: pd.Series, window: int = 7) -> Dict[str, Any]:
"""Detecta tendencias en la serie temporal"""
# Suavizado con media móvil
smoothed = data.rolling(window=window, center=True).mean()
# Calcular pendientes
slopes = []
for i in range(len(smoothed) - window):
x = np.arange(window)
y = smoothed.iloc[i:i+window].values
if not np.isnan(y).all():
slope, _, _, _, _ = stats.linregress(x, y)
slopes.append(slope)
else:
slopes.append(0)
# Clasificar tendencias
trend_threshold = np.std(slopes) * 0.5
trends = []
for slope in slopes:
if slope > trend_threshold:
trends.append("creciente")
elif slope < -trend_threshold:
trends.append("decreciente")
else:
trends.append("estable")
return {
"slopes": slopes,
"trends": trends,
"smoothed_data": smoothed.tolist(),
"trend_strength": np.std(slopes)
}
def detect_anomalies(self, data: pd.Series, method: str = "zscore") -> Dict[str, Any]:
"""Detecta anomalías en los datos"""
if method == "zscore":
z_scores = np.abs(stats.zscore(data.dropna()))
anomalies = z_scores > 2.5
anomaly_indices = data.index[anomalies].tolist()
elif method == "iqr":
Q1 = data.quantile(0.25)
Q3 = data.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
anomalies = (data < lower_bound) | (data > upper_bound)
anomaly_indices = data.index[anomalies].tolist()
elif method == "isolation":
# Usar DBSCAN como aproximación
values = data.values.reshape(-1, 1)
values_scaled = self.scaler.fit_transform(values)
clustering = DBSCAN(eps=0.5, min_samples=5).fit(values_scaled)
anomalies = clustering.labels_ == -1
anomaly_indices = data.index[anomalies].tolist()
return {
"anomaly_indices": anomaly_indices,
"anomaly_values": data.iloc[anomalies].tolist() if len(anomaly_indices) > 0 else [],
"total_anomalies": len(anomaly_indices)
}
def detect_seasonality(self, data: pd.Series, periods: List[int] = [7, 30, 365]) -> Dict[str, Any]:
"""Detecta patrones estacionales"""
seasonality_results = {}
for period in periods:
if len(data) >= period * 2:
# Autocorelación para detectar periodicidad
autocorr = []
for lag in range(1, min(period + 1, len(data) // 2)):
corr = data.autocorr(lag=lag)
autocorr.append(corr if not np.isnan(corr) else 0)
max_corr = max(autocorr) if autocorr else 0
seasonality_results[f"period_{period}"] = {
"strength": max_corr,
"detected": max_corr > 0.3
}
return seasonality_results
def find_peaks_valleys(self, data: pd.Series) -> Dict[str, Any]:
"""Encuentra picos y valles en la serie"""
# Encontrar picos
peaks, peak_properties = find_peaks(data.values, prominence=np.std(data) * 0.5)
# Encontrar valles (picos invertidos)
valleys, valley_properties = find_peaks(-data.values, prominence=np.std(data) * 0.5)
return {
"peaks": {
"indices": data.index[peaks].tolist(),
"values": data.iloc[peaks].tolist(),
"count": len(peaks)
},
"valleys": {
"indices": data.index[valleys].tolist(),
"values": data.iloc[valleys].tolist(),
"count": len(valleys)
}
}
def generate_forecast(self, data: pd.Series, periods: int = 30) -> Dict[str, Any]:
"""Genera pronóstico simple usando tendencia lineal"""
# Ajustar modelo lineal simple
x = np.arange(len(data))
y = data.values
# Remover NaN
mask = ~np.isnan(y)
if mask.sum() < 2:
return {"error": "Insuficientes datos para pronóstico"}
slope, intercept, r_value, _, _ = stats.linregress(x[mask], y[mask])
# Generar pronóstico
future_x = np.arange(len(data), len(data) + periods)
forecast = slope * future_x + intercept
# Calcular intervalos de confianza (simplificados)
residuals = y[mask] - (slope * x[mask] + intercept)
mse = np.mean(residuals ** 2)
std_error = np.sqrt(mse)
return {
"forecast_values": forecast.tolist(),
"confidence_upper": (forecast + 1.96 * std_error).tolist(),
"confidence_lower": (forecast - 1.96 * std_error).tolist(),
"r_squared": r_value ** 2,
"trend_slope": slope
}
# Instancia global del analizador
analyzer = TrendsAnalyzer()
def create_visualizations(df: pd.DataFrame, column: str, analysis_results: Dict[str, Any]) -> go.Figure:
"""Crea las visualizaciones de la serie temporal"""
fig = make_subplots(
rows=3, cols=2,
subplot_titles=(
'Serie Temporal Original', 'Tendencias Detectadas',
'Anomalías', 'Picos y Valles',
'Pronóstico', 'Distribución de Valores'
),
specs=[[{"secondary_y": False}, {"secondary_y": False}],
[{"secondary_y": False}, {"secondary_y": False}],
[{"secondary_y": False}, {"secondary_y": False}]]
)
# Serie original
fig.add_trace(
go.Scatter(x=df.index, y=df[column], name="Original", line=dict(color="blue")),
row=1, col=1
)
# Tendencias
if 'trends' in analysis_results:
smoothed = analysis_results['trends']['smoothed_data']
fig.add_trace(
go.Scatter(x=df.index, y=smoothed, name="Suavizada", line=dict(color="orange")),
row=1, col=2
)
# Anomalías
if 'anomalies' in analysis_results and analysis_results['anomalies']['anomaly_indices']:
anomaly_indices = analysis_results['anomalies']['anomaly_indices']
anomaly_values = analysis_results['anomalies']['anomaly_values']
fig.add_trace(
go.Scatter(x=df.index, y=df[column], name="Serie", line=dict(color="blue")),
row=2, col=1
)
fig.add_trace(
go.Scatter(
x=anomaly_indices, y=anomaly_values,
mode="markers", name="Anomalías",
marker=dict(color="red", size=8)
),
row=2, col=1
)
# Picos y valles
if 'peaks_valleys' in analysis_results:
fig.add_trace(
go.Scatter(x=df.index, y=df[column], name="Serie", line=dict(color="blue")),
row=2, col=2
)
peaks = analysis_results['peaks_valleys']['peaks']
if peaks['indices']:
fig.add_trace(
go.Scatter(
x=peaks['indices'], y=peaks['values'],
mode="markers", name="Picos",
marker=dict(color="green", size=8, symbol="triangle-up")
),
row=2, col=2
)
valleys = analysis_results['peaks_valleys']['valleys']
if valleys['indices']:
fig.add_trace(
go.Scatter(
x=valleys['indices'], y=valleys['values'],
mode="markers", name="Valles",
marker=dict(color="red", size=8, symbol="triangle-down")
),
row=2, col=2
)
# Pronóstico
if 'forecast' in analysis_results and 'error' not in analysis_results['forecast']:
forecast_data = analysis_results['forecast']
last_date = df.index[-1]
# Generar fechas futuras
if isinstance(last_date, pd.Timestamp):
future_dates = pd.date_range(
start=last_date + pd.Timedelta(days=1),
periods=len(forecast_data['forecast_values']),
freq='D'
)
else:
future_dates = range(len(df), len(df) + len(forecast_data['forecast_values']))
# Serie histórica
fig.add_trace(
go.Scatter(x=df.index, y=df[column], name="Histórico", line=dict(color="blue")),
row=3, col=1
)
# Pronóstico
fig.add_trace(
go.Scatter(
x=future_dates, y=forecast_data['forecast_values'],
name="Pronóstico", line=dict(color="red", dash="dash")
),
row=3, col=1
)
# Intervalos de confianza
fig.add_trace(
go.Scatter(
x=future_dates, y=forecast_data['confidence_upper'],
fill=None, mode='lines', line=dict(color='rgba(0,0,0,0)'),
showlegend=False
),
row=3, col=1
)
fig.add_trace(
go.Scatter(
x=future_dates, y=forecast_data['confidence_lower'],
fill='tonexty', mode='lines', line=dict(color='rgba(0,0,0,0)'),
name='Intervalo de Confianza', fillcolor='rgba(255,0,0,0.2)'
),
row=3, col=1
)
# Distribución
fig.add_trace(
go.Histogram(x=df[column], name="Distribución", nbinsx=30),
row=3, col=2
)
fig.update_layout(height=1200, showlegend=True, title_text="Análisis de Tendencias Temporales")
return fig
def analyze_time_series(file, column_name, trend_window, anomaly_method, forecast_periods):
""" Analiza una serie temporal para detectar tendencias, anomalías, estacionalidad y picos/valles."""
try:
# Leer archivo
if file.name.endswith('.csv'):
df = pd.read_csv(file.name)
elif file.name.endswith(('.xlsx', '.xls')):
df = pd.read_excel(file.name)
else:
return "Error: Formato de archivo no soportado", None, None
# Validar columna
if column_name not in df.columns:
return f"Error: Columna '{column_name}' no encontrada", None, None
# Intentar convertir índice a datetime si es posible
if 'fecha' in df.columns or 'date' in df.columns:
date_col = 'fecha' if 'fecha' in df.columns else 'date'
df[date_col] = pd.to_datetime(df[date_col])
df.set_index(date_col, inplace=True)
# Preparar serie
series = df[column_name].astype(float)
# Realizar análisis
results = {}
# Análisis de tendencias
results['trends'] = analyzer.detect_trends(series, window=trend_window)
# Detección de anomalías
results['anomalies'] = analyzer.detect_anomalies(series, method=anomaly_method)
# Detección de estacionalidad
results['seasonality'] = analyzer.detect_seasonality(series)
# Picos y valles
results['peaks_valleys'] = analyzer.find_peaks_valleys(series)
# Pronóstico
results['forecast'] = analyzer.generate_forecast(series, periods=forecast_periods)
# Guardar resultados
analysis_id = f"analysis_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
analysis_store[analysis_id] = {
'data': df,
'column': column_name,
'results': results
}
# Crear visualización
fig = create_visualizations(df, column_name, results)
# Generar reporte
report = generate_report(results, series)
return report, fig, analysis_id
except Exception as e:
return f"Error en el análisis: {str(e)}", None, None
def generate_report(results: Dict[str, Any], series: pd.Series) -> str:
"""Genera un reporte textual del análisis"""
report = "# Reporte de Análisis de Tendencias Temporales\n\n"
# Estadísticas básicas
report += "## Estadísticas Básicas\n"
report += f"- Número de observaciones: {len(series)}\n"
report += f"- Media: {series.mean():.2f}\n"
report += f"- Desviación estándar: {series.std():.2f}\n"
report += f"- Mínimo: {series.min():.2f}\n"
report += f"- Máximo: {series.max():.2f}\n\n"
# Tendencias
if 'trends' in results:
trend_strength = results['trends']['trend_strength']
report += "## Análisis de Tendencias\n"
report += f"- Fuerza de la tendencia: {trend_strength:.4f}\n"
if trend_strength > 0.1:
report += "- La serie presenta tendencias significativas\n\n"
else:
report += "- La serie presenta tendencias débiles o estables\n\n"
# Anomalías
if 'anomalies' in results:
anomaly_count = results['anomalies']['total_anomalies']
report += "## Detección de Anomalías\n"
report += f"- Anomalías detectadas: {anomaly_count}\n"
report += f"- Porcentaje de anomalías: {(anomaly_count/len(series)*100):.1f}%\n\n"
# Estacionalidad
if 'seasonality' in results:
report += "## Análisis de Estacionalidad\n"
for period, data in results['seasonality'].items():
period_name = period.replace('period_', '')
if data['detected']:
report += f"- Patrón estacional de {period_name} períodos detectado (fuerza: {data['strength']:.3f})\n"
report += "\n"
# Picos y valles
if 'peaks_valleys' in results:
peaks_count = results['peaks_valleys']['peaks']['count']
valleys_count = results['peaks_valleys']['valleys']['count']
report += "## Picos y Valles\n"
report += f"- Picos detectados: {peaks_count}\n"
report += f"- Valles detectados: {valleys_count}\n\n"
# Pronóstico
if 'forecast' in results and 'error' not in results['forecast']:
r_squared = results['forecast']['r_squared']
slope = results['forecast']['trend_slope']
report += "## Pronóstico\n"
report += f"- R² del modelo: {r_squared:.3f}\n"
report += f"- Pendiente de tendencia: {slope:.4f}\n"
if slope > 0:
report += "- Tendencia proyectada: Creciente\n"
elif slope < 0:
report += "- Tendencia proyectada: Decreciente\n"
else:
report += "- Tendencia proyectada: Estable\n"
return report
# Funciones MCP para Gradio 5
def mcp_analyze_trends(data: List[float], dates: Optional[List[str]] = None,
window: int = 7, anomaly_method: str = "zscore") -> str:
""" Analiza una serie temporal para detectar tendencias, anomalías, estacionalidad y picos/valles.
Parámetros:
- data (List[float]): Serie de valores numéricos a analizar.
- dates (List[str], opcional): Fechas correspondientes a los datos. Si se omite, se usará índice por posición.
- window (int): Ventana de suavizado para detección de tendencias.
- anomaly_method (str): Método de detección de anomalías. Opciones: "zscore", "iqr", "isolation".
Retorna:
- str: JSON con resumen del análisis, resultados completos y un ID único."""
def make_json_serializable(obj):
"""Convierte objetos no serializables a JSON"""
if isinstance(obj, np.integer):
return int(obj)
elif isinstance(obj, np.floating):
return float(obj)
elif isinstance(obj, np.ndarray):
return obj.tolist()
elif isinstance(obj, pd.Timestamp):
return obj.isoformat()
elif isinstance(obj, pd.Index):
return obj.tolist()
elif isinstance(obj, dict):
return {key: make_json_serializable(value) for key, value in obj.items()}
elif isinstance(obj, list):
return [make_json_serializable(item) for item in obj]
elif isinstance(obj, (bool, np.bool_)):
return bool(obj)
elif pd.isna(obj):
return None
else:
return obj
try:
# Crear serie pandas
if dates:
index = pd.to_datetime(dates)
series = pd.Series(data, index=index)
else:
series = pd.Series(data)
# Realizar análisis
results = {}
results['trends'] = analyzer.detect_trends(series, window=window)
results['anomalies'] = analyzer.detect_anomalies(series, method=anomaly_method)
results['seasonality'] = analyzer.detect_seasonality(series)
results['peaks_valleys'] = analyzer.find_peaks_valleys(series)
# Convertir a JSON serializable
results_serializable = make_json_serializable(results)
# Guardar análisis
analysis_id = f"mcp_analysis_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
analysis_store[analysis_id] = {
'data': series.to_frame('value'),
'column': 'value',
'results': results
}
return json.dumps({
"analysis_id": analysis_id,
"summary": {
"total_points": len(data),
"anomalies_detected": int(results['anomalies']['total_anomalies']),
"peaks_count": int(results['peaks_valleys']['peaks']['count']),
"valleys_count": int(results['peaks_valleys']['valleys']['count']),
"trend_strength": float(results['trends']['trend_strength'])
},
"results": results_serializable
}, indent=2)
except Exception as e:
return f"Error en análisis MCP: {str(e)}"
def mcp_get_analysis_report(analysis_id: str) -> str:
"""Recupera el reporte textual completo de un análisis previo a partir de su ID.
Parámetros:
- analysis_id (str): Identificador único del análisis almacenado, obtenido de mcp_analyze_trends. Formato: "mcp_analysis_YYYYMMDD_HHMMSS"
Retorna:
- str: Reporte completo en formato Markdown con:
* Estadísticas básicas (media, desviación, min/max)
* Análisis de tendencias (fuerza y dirección)
* Detección de anomalías (cantidad y porcentaje)
* Análisis de estacionalidad (patrones detectados)
* Picos y valles (conteo de extremos)
* Información de pronóstico si está disponible
En caso de error o ID no encontrado, devuelve mensaje de error."""
try:
if analysis_id not in analysis_store:
return "Error: Análisis no encontrado"
stored_analysis = analysis_store[analysis_id]
series = stored_analysis['data'][stored_analysis['column']]
results = stored_analysis['results']
report = generate_report(results, series)
return report
except Exception as e:
return f"Error obteniendo reporte: {str(e)}"
def mcp_forecast_series(data: List[float], periods: int = 30) -> str:
""" Genera un pronóstico lineal para una serie temporal basado en regresión lineal simple.
Parámetros:
- data (List[float]): Serie de datos históricos numéricos. Mínimo 2 valores requeridos. Ejemplo: [100, 105, 110, 108, 115, 120]
- periods (int): Número de períodos futuros a predecir (5-100). Por defecto: 30
Retorna:
- str: JSON con pronóstico que incluye:
* forecast_values: Array de valores pronosticados
* confidence_upper: Límite superior del intervalo de confianza (95%)
* confidence_lower: Límite inferior del intervalo de confianza (95%)
* r_squared: Coeficiente de determinación del modelo (0-1, donde 1 es perfecto)
* trend_slope: Pendiente de la tendencia (positivo=creciente, negativo=decreciente, ~0=estable)
Usa regresión lineal simple con intervalos de confianza del 95%. En caso de datos insuficientes, retorna error."""
try:
series = pd.Series(data)
forecast_results = analyzer.generate_forecast(series, periods=periods)
return json.dumps(forecast_results, indent=2)
except Exception as e:
return f"Error en pronóstico: {str(e)}"
# Interfaz Gradio con funciones MCP integradas
def create_gradio_interface():
with gr.Blocks(title="Análisis de Tendencias Temporales MCP", theme=gr.themes.Soft()) as interface:
gr.Markdown("# 📈 Análisis de Tendencias Temporales con MCP")
gr.Markdown("Servidor de análisis con capacidades MCP integradas para series temporales.")
with gr.Tab("🔍 Análisis de Archivos"):
with gr.Row():
with gr.Column(scale=1):
file_input = gr.File(
label="Subir archivo (CSV/Excel)",
file_types=[".csv", ".xlsx", ".xls"]
)
column_input = gr.Textbox(
label="Nombre de la columna a analizar",
placeholder="ej: ventas, temperatura, precio"
)
with gr.Row():
trend_window = gr.Slider(
minimum=3, maximum=30, value=7,
label="Ventana para detectar tendencias"
)
forecast_periods = gr.Slider(
minimum=5, maximum=100, value=30,
label="Períodos a pronosticar"
)
anomaly_method = gr.Dropdown(
choices=["zscore", "iqr", "isolation"],
value="zscore",
label="Método de detección de anomalías"
)
analyze_btn = gr.Button("🔍 Analizar Serie Temporal", variant="primary")
with gr.Column(scale=2):
report_output = gr.Markdown(label="Reporte de Análisis")
analysis_id_output = gr.Textbox(label="ID de Análisis", visible=True)
with gr.Row():
plot_output = gr.Plot(label="Visualizaciones")
analyze_btn.click(
analyze_time_series,
inputs=[file_input, column_input, trend_window, anomaly_method, forecast_periods],
outputs=[report_output, plot_output, analysis_id_output]
)
with gr.Tab("🤖 Funciones MCP"):
gr.Markdown("## Funciones MCP para Análisis Programático")
with gr.Row():
with gr.Column():
gr.Markdown("### Analizar Serie Temporal")
mcp_data_input = gr.Textbox(
label="Datos (JSON array)",
placeholder='[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]',
lines=3
)
mcp_dates_input = gr.Textbox(
label="Fechas (opcional, JSON array)",
placeholder='["2024-01-01", "2024-01-02", ...]',
lines=2
)
mcp_window = gr.Number(label="Ventana", value=7)
mcp_method = gr.Dropdown(
choices=["zscore", "iqr", "isolation"],
value="zscore",
label="Método de anomalías"
)
mcp_analyze_btn = gr.Button("📊 Analizar con MCP")
with gr.Column():
gr.Markdown("### Obtener Reporte")
mcp_analysis_id = gr.Textbox(
label="ID de Análisis",
placeholder="analysis_20241201_123456"
)
mcp_report_btn = gr.Button("📄 Obtener Reporte")
gr.Markdown("### Generar Pronóstico")
mcp_forecast_data = gr.Textbox(
label="Datos para Pronóstico",
placeholder='[10, 12, 14, 16, 18, 20]',
lines=2
)
mcp_periods = gr.Number(label="Períodos", value=10)
mcp_forecast_btn = gr.Button("🔮 Pronosticar")
mcp_output = gr.Textbox(
label="Resultado MCP",
lines=15,
show_copy_button=True
)
def handle_mcp_analyze(data_str, dates_str, window, method):
"""Función auxiliar para interfaz Gradio. Convierte strings JSON a listas y llama mcp_analyze_trends.
Parámetros:
- data_str (str): JSON string con array de números
- dates_str (str): JSON string con array de fechas (opcional)
- window (int): Ventana de tendencias
- method (str): Método de detección de anomalías
Retorna resultado de mcp_analyze_trends o mensaje de error."""
try:
data = json.loads(data_str)
dates = json.loads(dates_str) if dates_str.strip() else None
return mcp_analyze_trends(data, dates, int(window), method)
except Exception as e:
return f"Error: {str(e)}"
def handle_mcp_forecast(data_str, periods):
"""Función auxiliar para interfaz Gradio. Convierte string JSON a lista y llama mcp_forecast_series.
Parámetros:
- data_str (str): JSON string con array de números históricos
- periods (int): Períodos a pronosticar
Retorna resultado de mcp_forecast_series o mensaje de error."""
try:
data = json.loads(data_str)
return mcp_forecast_series(data, int(periods))
except Exception as e:
return f"Error: {str(e)}"
mcp_analyze_btn.click(
handle_mcp_analyze,
inputs=[mcp_data_input, mcp_dates_input, mcp_window, mcp_method],
outputs=mcp_output
)
mcp_report_btn.click(
mcp_get_analysis_report,
inputs=mcp_analysis_id,
outputs=mcp_output
)
mcp_forecast_btn.click(
handle_mcp_forecast,
inputs=[mcp_forecast_data, mcp_periods],
outputs=mcp_output
)
with gr.Tab("📚 Documentación"):
gr.Markdown("""
## Available MCP Functions
### 1. `analyze_trends`
Analyzes a time series to detect trends, anomalies, and patterns.
**Parameters:**
- `data`: Array of numbers (required)
- `dates`: Array of dates (optional)
- `window`: Trend smoothing window (default: 7)
- `anomaly_method`: Detection method ("zscore", "iqr", "isolation")
**Returns:** JSON with analysis ID and full results
### 2. `get_analysis_report`
Retrieves the textual report of a previous analysis.
**Parameters:**
- `analysis_id`: Analysis ID (required)
**Returns:** Report in Markdown format
### 3. `forecast_series`
Generates forecasts for a time series.
**Parameters:**
- `data`: Array of historical numbers (required)
- `periods`: Number of periods to forecast (default: 30)
**Returns:** JSON with forecasted values and confidence intervals
## Analysis Features
- 🔍 **Trend detection** with moving average smoothing
- ⚠️ **Anomaly identification** using multiple methods
- 📅 **Seasonality analysis** for recurring patterns
- 📊 **Peak and valley detection** using scipy
- 🔮 **Linear forecasting** with confidence intervals
- 📈 **Interactive visualizations** with Plotly
## Use from External Applications
This server can be used as a full MCP server, providing
time series analysis through structured function calls.
""")
return interface
def main():
"""Función principal"""
print("🚀 Iniciando Servidor de Análisis de Tendencias Temporales")
print("📊 Con capacidades MCP integradas en Gradio 5")
# Crear y lanzar interfaz con MCP habilitado
interface = create_gradio_interface()
# Lanzar con MCP=True para habilitar capacidades MCP en Gradio 5
interface.launch(
share=True,
mcp_server=True # Habilita capacidades MCP en Gradio 5
)
if __name__ == "__main__":
main()