Skip to content

Commit d7f5f79

Browse files
ci: add ruff lint, MIT license and CI badges
- Add GitHub Actions (ruff lint/format + smoke compile/import) - Add MIT LICENSE, .editorconfig, .gitattributes and dependabot - Add CI/License badges to README - Fix ruff issues in graph_builder.py
1 parent 0635f3e commit d7f5f79

7 files changed

Lines changed: 150 additions & 31 deletions

File tree

.editorconfig

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
insert_final_newline = true
7+
trim_trailing_whitespace = true
8+
9+
[*.{py,pyw}]
10+
indent_style = space
11+
indent_size = 4
12+
13+
[*.{yml,yaml,json,toml}]
14+
indent_style = space
15+
indent_size = 2
16+

.gitattributes

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Normalizar saltos de línea a LF en texto, mantener binarios intactos
2+
* text=auto
3+
4+
*.png binary
5+
*.jpg binary
6+

.github/dependabot.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "pip"
4+
directory: "/"
5+
schedule:
6+
interval: "weekly"
7+
open-pull-requests-limit: 5
8+
9+
- package-ecosystem: "github-actions"
10+
directory: "/"
11+
schedule:
12+
interval: "weekly"
13+
open-pull-requests-limit: 5
14+

.github/workflows/ci.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
concurrency:
9+
group: ci-${{ github.workflow }}-${{ github.ref }}
10+
cancel-in-progress: true
11+
12+
jobs:
13+
lint:
14+
name: Ruff (lint + format)
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-python@v5
19+
with:
20+
python-version: "3.12"
21+
cache: pip
22+
- name: Install dependencies
23+
run: |
24+
python -m pip install --upgrade pip
25+
pip install ruff
26+
- name: Ruff check
27+
run: ruff check .
28+
- name: Ruff format check
29+
run: ruff format --check .
30+
31+
build:
32+
name: Smoke (compile + import)
33+
runs-on: ubuntu-latest
34+
steps:
35+
- uses: actions/checkout@v4
36+
- uses: actions/setup-python@v5
37+
with:
38+
python-version: "3.12"
39+
- name: Compile all Python sources
40+
run: python -m compileall -q src app.py app_mapas.py 2>/dev/null || python -m compileall -q .
41+
- name: Import distribution modules
42+
run: python -c "import sys; sys.path.insert(0, 'src'); import graph_builder; print('graph_builder OK')"

LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Álvaro Salinas
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
22+

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
## Executive Summary & Decision Making
88

9+
[![CI](https://github.com/alvarosalinaso/tactical-narrative-graph-analysis/actions/workflows/ci.yml/badge.svg)](https://github.com/alvarosalinaso/tactical-narrative-graph-analysis/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
10+
911
Este proyecto diseña e implementa una infraestructura avanzada de **Ciencia de Redes (Complex Network Analysis - CNA)** y **Teoría de Grafos** aplicada al rendimiento deportivo de élite. Mediante la modelación matemática de un partido de fútbol no como eventos independientes aislados, sino como un **Sistema Complejo Dinámico**, el algoritmo deconstruye las interacciones colectivas del equipo. Esto permite parametrizar de forma cuantitativa la circulación de información (el balón), identificar vulnerabilidades estructurales en la red y optimizar la resiliencia táctica colectiva.
1012

1113
El análisis de grafos tácticos capacita a Directores Técnicos, Analistas de Rendimiento y Departamentos de Inteligencia Deportiva para tomar **decisiones tácticas de alto nivel**:

src/graph_builder.py

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,87 @@
1-
import pandas as pd
1+
import os
2+
23
import networkx as nx
4+
import pandas as pd
35
from pyvis.network import Network
4-
import os
56

67
# Simularemos un generador offline primero para asegurar estabilidad inmediata.
7-
# En producción, usaríamos `from statsbombpy import sb`
8+
# En producción, usaríamos `from statsbombpy import sb`
89
# y haríamos: sb.events(match_id=3750201)
910

11+
1012
def build_graph(data: pd.DataFrame) -> nx.DiGraph:
1113
"""Extrae las conexiones dirigidas de Pases y crea el Grafo Matemático"""
1214
G = nx.DiGraph()
13-
15+
1416
# Agregar Nodos y bordes con peso (Weight = N° de Pases)
1517
for index, row in data.iterrows():
16-
source = row['Passer']
17-
target = row['Receiver']
18-
18+
source = row["Passer"]
19+
target = row["Receiver"]
20+
1921
if G.has_edge(source, target):
20-
G[source][target]['weight'] += 1
22+
G[source][target]["weight"] += 1
2123
else:
2224
G.add_edge(source, target, weight=1)
23-
25+
2426
return G
2527

28+
2629
def analyze_and_visualize(G: nx.DiGraph, output_filename="grafo_tactico.html"):
2730
"""
2831
Calcula Betweenness Centrality (El dictador o 'broker' táctico del equipo)
2932
y renderiza el mapa en HTML interactivo
3033
"""
3134
# 1. Análisis de centralidad (Matemática Pura)
32-
centrality = nx.betweenness_centrality(G, weight='weight')
33-
35+
centrality = nx.betweenness_centrality(G, weight="weight")
36+
3437
# 2. Configurar motor visual PyVis
35-
net = Network(height="600px", width="100%", bgcolor="#222222", font_color="white", directed=True)
36-
38+
net = Network(
39+
height="600px",
40+
width="100%",
41+
bgcolor="#222222",
42+
font_color="white",
43+
directed=True,
44+
)
45+
3746
# Transformar a formato visual
3847
for node in G.nodes():
3948
# El tamaño del jugador en el mapa dependerá de cuánto es el 'puente' táctico
40-
size = centrality.get(node, 0.01) * 200 + 10
41-
net.add_node(node, label=node, title=f"Betweenness: {centrality.get(node, 0.01):.2f}", size=size)
42-
49+
size = centrality.get(node, 0.01) * 200 + 10
50+
net.add_node(
51+
node,
52+
label=node,
53+
title=f"Betweenness: {centrality.get(node, 0.01):.2f}",
54+
size=size,
55+
)
56+
4357
for source, target, data in G.edges(data=True):
44-
weight = data['weight']
58+
weight = data["weight"]
4559
net.add_edge(source, target, value=weight, title=f"{weight} pases")
46-
60+
4761
# Guardar en local (Se puede abrir en cualquier navegador web)
4862
os.makedirs("output", exist_ok=True)
4963
out_path = os.path.join("output", output_filename)
5064
net.write_html(out_path)
5165
print(f"Grafo interactivo renderizado con éxito en: {out_path}")
5266

67+
5368
if __name__ == "__main__":
5469
# Generamos un dataset esqueleto con base en tus propias estadísticas de passing.csv del United
5570
# Esto asegura de que corra a la primera (Plug-and-play)
56-
mock_data = pd.DataFrame([
57-
{"Passer": "Onana", "Receiver": "Martinez"},
58-
{"Passer": "Onana", "Receiver": "Dalot"},
59-
{"Passer": "Martinez", "Receiver": "Bruno Fernandes"},
60-
{"Passer": "Martinez", "Receiver": "Mainoo"},
61-
{"Passer": "Dalot", "Receiver": "Bruno Fernandes"},
62-
{"Passer": "Mainoo", "Receiver": "Bruno Fernandes"},
63-
{"Passer": "Bruno Fernandes", "Receiver": "Garnacho"},
64-
{"Passer": "Bruno Fernandes", "Receiver": "Garnacho"},
65-
{"Passer": "Bruno Fernandes", "Receiver": "Hojlund"},
66-
{"Passer": "Garnacho", "Receiver": "Hojlund"},
67-
])
68-
71+
mock_data = pd.DataFrame(
72+
[
73+
{"Passer": "Onana", "Receiver": "Martinez"},
74+
{"Passer": "Onana", "Receiver": "Dalot"},
75+
{"Passer": "Martinez", "Receiver": "Bruno Fernandes"},
76+
{"Passer": "Martinez", "Receiver": "Mainoo"},
77+
{"Passer": "Dalot", "Receiver": "Bruno Fernandes"},
78+
{"Passer": "Mainoo", "Receiver": "Bruno Fernandes"},
79+
{"Passer": "Bruno Fernandes", "Receiver": "Garnacho"},
80+
{"Passer": "Bruno Fernandes", "Receiver": "Garnacho"},
81+
{"Passer": "Bruno Fernandes", "Receiver": "Hojlund"},
82+
{"Passer": "Garnacho", "Receiver": "Hojlund"},
83+
]
84+
)
85+
6986
grafo = build_graph(mock_data)
7087
analyze_and_visualize(grafo)

0 commit comments

Comments
 (0)