Skip to content

Commit a9325d3

Browse files
committed
Refactor to installable package, add realistic data generator, tests and CI
Software engineering - Move logic to a `src/inventory` package (config, data_gen, kpis) with a clean public API. Notebook and Streamlit app now import the same code, so cálculos quedan garantizados consistentes - Add pyproject.toml (setuptools build, ruff + pytest config, optional extras `[dev]` and `[app]`) - Add `tests/` with 17 pytest cases covering generator invariants, KPI monotonicity wrt service level / lead time, traffic-light boundaries, determinism and zero-variance edge cases - Add GitHub Actions CI: ruff + pytest on Python 3.10/3.11/3.12 Data realism - Replace the trivial Poisson + independent-normal-stock generator with one that produces an ABC curve (Pareto-distributed baselines), weekly + annual seasonality, per-SKU trend, promotional spikes, log-normal lead times and coherent stock evolution under an (s, Q) replenishment policy - Result: stockouts emerge from the simulation (2.7k stockout-days in the default dataset), so KPIs and the traffic light expose meaningful risk Notebook / dashboard wiring - Notebook bootstraps `src/` onto sys.path and consumes the package - Streamlit app imports KPI and config helpers from the package instead of re-implementing them - Regenerate inventory_transactions.csv and inventario_kpis.xlsx with the new generator (54,750 rows: 50 products × 3 warehouses × 365 days) README - New sections: package API, realistic generator details, tests + CI - Add CI / Python / ruff badges - Update structure, stack and reproduce-the-analysis instructions
1 parent 701a673 commit a9325d3

15 files changed

Lines changed: 55488 additions & 9192 deletions

.github/workflows/ci.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, "claude/**"]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
name: Lint & test
12+
runs-on: ubuntu-latest
13+
14+
strategy:
15+
matrix:
16+
python-version: ["3.10", "3.11", "3.12"]
17+
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
22+
- name: Set up Python ${{ matrix.python-version }}
23+
uses: actions/setup-python@v5
24+
with:
25+
python-version: ${{ matrix.python-version }}
26+
cache: pip
27+
28+
- name: Install dependencies
29+
run: |
30+
python -m pip install --upgrade pip
31+
pip install -e ".[dev]"
32+
33+
- name: Lint with ruff
34+
run: ruff check src tests
35+
36+
- name: Run tests
37+
run: pytest -v

README.md

Lines changed: 105 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Optimización y Análisis de Inventario
22

3-
[![Python](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://www.python.org/)
3+
[![CI](https://github.com/NJerez-dev/Logistics-data-portfolio/actions/workflows/ci.yml/badge.svg)](https://github.com/NJerez-dev/Logistics-data-portfolio/actions/workflows/ci.yml)
4+
[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
5+
[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
46
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
57
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/NJerez-dev/Logistics-data-portfolio/blob/main/inventory_analysis.ipynb)
68
[![Streamlit](https://static.streamlit.io/badges/streamlit_badge_black_white.svg)](https://streamlit.io/cloud)
@@ -14,11 +16,14 @@ bodega y categoría.
1416

1517
- [Objetivo](#objetivo)
1618
- [Dashboard interactivo](#dashboard-interactivo)
19+
- [El paquete `inventory`](#el-paquete-inventory)
20+
- [Generador de datos realista](#generador-de-datos-realista)
1721
- [Stack técnico](#stack-técnico)
1822
- [Estructura del repositorio](#estructura-del-repositorio)
1923
- [Cómo reproducir el análisis](#cómo-reproducir-el-análisis)
2024
- [Análisis realizado](#análisis-realizado)
2125
- [KPIs calculados](#kpis-calculados)
26+
- [Tests y CI](#tests-y-ci)
2227
- [Conclusiones clave](#conclusiones-clave)
2328
- [Próximos pasos](#próximos-pasos)
2429
- [Licencia](#licencia)
@@ -27,12 +32,13 @@ bodega y categoría.
2732

2833
Mostrar un flujo end-to-end de análisis de datos logísticos:
2934

30-
1. **Generación** de un dataset sintético realista (50 productos × 3 bodegas × 180 días).
35+
1. **Generación** de un dataset sintético realista (50 productos × 3 bodegas × 365 días, con estacionalidad anual y semanal, curva ABC, lead times log-normales y stock evolucionado de forma coherente).
3136
2. **Exploración** de ventas por categoría, bodega y producto.
3237
3. **Detección** de productos en riesgo de quiebre de stock.
33-
4. **Cálculo** de KPIs de reposición (ROP, Stock de Seguridad) con un nivel de servicio del 95%.
38+
4. **Cálculo** de KPIs de reposición (ROP, Stock de Seguridad) con un nivel de servicio del 95 %.
3439
5. **Visualización** tipo "semáforo" del estado del inventario.
35-
6. **Reporting** en un libro Excel multi-hoja con gráficos embebidos.
40+
6. **Reporting** en un libro Excel multi-hoja y en un **dashboard interactivo de Streamlit**.
41+
7. **Calidad de código**: paquete instalable con `pyproject.toml`, suite de `pytest` y CI con GitHub Actions.
3642

3743
## Dashboard interactivo
3844

@@ -70,6 +76,59 @@ Abre tu navegador en [http://localhost:8501](http://localhost:8501).
7076
| **🚦 Estado de inventario** | Semáforo por producto (rojo/amarillo/verde) + ROP overlay. |
7177
| **📋 KPIs por producto/bodega** | Tabla detallada filtrable + descarga en CSV. |
7278

79+
## El paquete `inventory`
80+
81+
La lógica de negocio vive en `src/inventory/` como paquete Python instalable
82+
y testeado, no en celdas de notebook copiadas.
83+
84+
```python
85+
from inventory import (
86+
generate_dataset,
87+
compute_product_kpis,
88+
compute_warehouse_kpis,
89+
status_for,
90+
SERVICE_LEVELS,
91+
)
92+
93+
df = generate_dataset() # dataset sintético realista
94+
kpis = compute_product_kpis(df, z=1.645) # SS y ROP por producto
95+
status_for(stock=10, rop=20, avg_demand=2, buffer_days=7)
96+
# → '🔴 Crítico'
97+
```
98+
99+
Se instala en modo editable con:
100+
101+
```bash
102+
pip install -e ".[dev]"
103+
```
104+
105+
| Módulo | Responsabilidad |
106+
| ----------------------- | -------------------------------------------------------- |
107+
| `inventory.config` | Constantes (Z-scores, categorías, bodegas, etiquetas). |
108+
| `inventory.data_gen` | Generador sintético con estacionalidad y stock coherente.|
109+
| `inventory.kpis` | `compute_product_kpis`, `compute_warehouse_kpis`, etc. |
110+
111+
El notebook y el dashboard de Streamlit consumen esta misma API, por lo que
112+
los cálculos están garantizados consistentes entre ambos.
113+
114+
## Generador de datos realista
115+
116+
El módulo `inventory.data_gen` reemplaza la simulación trivial original
117+
(Poisson uniforme + stock independiente) por algo que se parece a retail real:
118+
119+
| Característica | Detalle |
120+
| ------------------------ | ------------------------------------------------------------------------ |
121+
| **Curva ABC** | Demanda baseline ~ Pareto: ~20 % de los SKUs concentran la mayor parte de los ingresos. |
122+
| **Estacionalidad anual** | Pico en noviembre-diciembre (Black Friday + holidays), valle en febrero. |
123+
| **Estacionalidad semanal** | Pico de lunes a jueves, caída los fines de semana. |
124+
| **Tendencia por SKU** | Cada combinación producto-bodega tiene una pendiente lineal aleatoria. |
125+
| **Promociones** | ~2 % de los días tienen un uplift de demanda × 2,5. |
126+
| **Stock coherente** | `stock_t = max(0, stock_{t−1} − sold_t + arrivals_t)` — los quiebres son **emergentes**, no un artefacto de muestreo. |
127+
| **Lead times log-normales** | Cada orden de reposición tarda un tiempo distinto en llegar. |
128+
| **Política (s, Q)** | Cuando `stock < ROP`, se dispara una orden de tamaño `Q`. |
129+
130+
Todos estos parámetros se exponen en `GenerationParams` para experimentación.
131+
73132
## Stack técnico
74133

75134
| Herramienta | Uso |
@@ -87,12 +146,22 @@ Abre tu navegador en [http://localhost:8501](http://localhost:8501).
87146

88147
```
89148
.
149+
├── src/inventory/ # Paquete instalable
150+
│ ├── __init__.py
151+
│ ├── config.py # Constantes (Z, categorías, etiquetas)
152+
│ ├── data_gen.py # Generador sintético realista
153+
│ └── kpis.py # Cálculo de SS / ROP / semáforo
154+
├── tests/ # Suite de pytest
155+
│ ├── test_data_gen.py
156+
│ └── test_kpis.py
157+
├── .github/workflows/ci.yml # Lint + tests en GitHub Actions
90158
├── inventory_analysis.ipynb # Notebook principal con todo el análisis
91159
├── streamlit_app.py # Dashboard interactivo
92160
├── .streamlit/config.toml # Tema y configuración del dashboard
93161
├── inventory_transactions.csv # Dataset sintético generado (entrada)
94162
├── inventario_kpis.xlsx # Reporte final multi-hoja (salida)
95-
├── requirements.txt # Dependencias del proyecto
163+
├── pyproject.toml # Empaquetado + configuración de ruff/pytest
164+
├── requirements.txt # Dependencias para el dashboard / notebook
96165
├── LICENSE # Licencia MIT
97166
└── README.md # Este archivo
98167
```
@@ -109,6 +178,8 @@ Abre tu navegador en [http://localhost:8501](http://localhost:8501).
109178
```bash
110179
python -m venv .venv
111180
source .venv/bin/activate # En Windows: .venv\Scripts\activate
181+
pip install -e ".[dev,app]" # paquete + dependencias de tests y dashboard
182+
# o, si solo quieres correr el notebook / dashboard:
112183
pip install -r requirements.txt
113184
```
114185

@@ -128,7 +199,9 @@ Abre tu navegador en [http://localhost:8501](http://localhost:8501).
128199
### 1. Generación de datos
129200
- 50 productos en 4 categorías (`Electronics`, `Home`, `Industrial`, `Food`).
130201
- 3 bodegas (`Santiago`, `Valparaiso`, `Concepcion`).
131-
- 180 días de transacciones diarias con demanda Poisson y stock normal.
202+
- 365 días con demanda Poisson modulada por estacionalidad anual y semanal,
203+
curva ABC, promociones y stock evolucionado vía política (s, Q).
204+
- Ver [Generador de datos realista](#generador-de-datos-realista) para más detalle.
132205

133206
### 2. Análisis de ventas
134207
- Ingresos totales y unidades vendidas.
@@ -162,6 +235,27 @@ Reorder Point (ROP) = demanda_promedio_diaria · lead_time + SS
162235
Los KPIs se agregan también a nivel de **categoría × bodega** para apoyar
163236
decisiones tácticas de reabastecimiento.
164237

238+
## Tests y CI
239+
240+
Suite con `pytest` que cubre:
241+
242+
- **Generador**: schema, ausencia de nulos, `units_sold ≥ 0`, `stock_level ≥ 0`,
243+
determinismo bajo el mismo seed, presencia de la curva ABC y de la
244+
estacionalidad anual.
245+
- **KPIs**: `safety_stock ≥ 0`, monotonía respecto al nivel de servicio y al
246+
lead time, fronteras del semáforo, estabilidad ante varianza nula.
247+
248+
Ejecútalos localmente:
249+
250+
```bash
251+
pip install -e ".[dev]"
252+
ruff check src tests
253+
pytest -v
254+
```
255+
256+
GitHub Actions (`.github/workflows/ci.yml`) corre lint + tests automáticamente
257+
en cada push / PR sobre Python 3.10, 3.11 y 3.12.
258+
165259
## Conclusiones clave
166260

167261
1. La categoría **Industrial** concentra el mayor volumen de ingresos.
@@ -173,10 +267,11 @@ decisiones tácticas de reabastecimiento.
173267

174268
## Próximos pasos
175269

176-
- Incorporar **estacionalidad** en la simulación de demanda.
177-
- Modelar **lead times variables** con distribución log-normal.
178-
- Comparar la política `(s, S)` vs. `(R, Q)` para distintos productos.
179-
- Conectar el pipeline a un dashboard en **Streamlit** o **Power BI**.
270+
- **Forecasting** de demanda comparando baseline (media móvil) vs. Prophet vs. LightGBM.
271+
- **Simulación Monte Carlo** del fill rate real bajo distintas políticas.
272+
- **Comparación de políticas** `(s, S)` vs. `(R, Q)` vs. base-stock.
273+
- **Clasificación ABC/XYZ** para segmentar la lógica de reposición.
274+
- **Anomaly detection** en las series de ventas.
180275

181276
## Licencia
182277

inventario_kpis.xlsx

1.85 MB
Binary file not shown.

inventory_analysis.ipynb

Lines changed: 32 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -63,104 +63,45 @@
6363
},
6464
{
6565
"cell_type": "code",
66-
"source": [
67-
"\n",
68-
"np.random.seed(42)\n",
69-
"# parámetros base\n",
70-
"n_products = 50\n",
71-
"n_days = 180\n",
72-
"warehouses = ['Santiago', 'Valparaiso', 'Concepcion']\n"
73-
],
74-
"metadata": {
75-
"id": "oVyMsK07_Qqb"
76-
},
66+
"metadata": {},
7767
"execution_count": null,
78-
"outputs": []
79-
},
80-
{
81-
"cell_type": "code",
68+
"outputs": [],
8269
"source": [
83-
"# crear catálogo productos\n",
84-
"products = pd.DataFrame({\n",
85-
" 'product_id': range(1, n_products+1),\n",
86-
" 'product_name': [f'Product_{i}' for i in range(1, n_products+1)],\n",
87-
" 'category': np.random.choice(\n",
88-
" ['Electronics','Home','Industrial','Food'],\n",
89-
" n_products\n",
90-
" ),\n",
91-
" 'unit_price': np.random.randint(5000, 50000, n_products),\n",
92-
" 'reorder_point': np.random.randint(20, 100, n_products),\n",
93-
" 'supplier_lead_time': np.random.randint(3, 15, n_products)\n",
94-
"})"
95-
],
96-
"metadata": {
97-
"id": "GWIl9ySz_aa6"
98-
},
99-
"execution_count": null,
100-
"outputs": []
70+
"# Hace que el paquete local `inventory` sea importable (en Colab,\n",
71+
"# clona primero el repo o ejecuta este notebook desde la raíz del proyecto).\n",
72+
"import sys\n",
73+
"from pathlib import Path\n",
74+
"\n",
75+
"src_dir = Path.cwd() / 'src'\n",
76+
"if src_dir.exists() and str(src_dir) not in sys.path:\n",
77+
" sys.path.insert(0, str(src_dir))\n",
78+
"\n",
79+
"from inventory import (\n",
80+
" GenerationParams,\n",
81+
" generate_dataset,\n",
82+
" compute_product_kpis,\n",
83+
" compute_warehouse_kpis,\n",
84+
")\n"
85+
]
10186
},
10287
{
10388
"cell_type": "code",
104-
"source": [
105-
"# generar fechas\n",
106-
"dates = pd.date_range(start='2024-01-01', periods=n_days)\n",
107-
"\n",
108-
"data = []\n",
109-
"\n",
110-
"for date in dates:\n",
111-
" for _, product in products.iterrows():\n",
112-
"\n",
113-
" units_sold = np.random.poisson(5)\n",
114-
" stock_level = max(0, np.random.normal(100, 30))\n",
115-
"\n",
116-
" warehouse = np.random.choice(warehouses)\n",
117-
"\n",
118-
" revenue = units_sold * product['unit_price']\n",
119-
"\n",
120-
" data.append([\n",
121-
" date,\n",
122-
" product['product_id'],\n",
123-
" product['product_name'],\n",
124-
" product['category'],\n",
125-
" warehouse,\n",
126-
" units_sold,\n",
127-
" product['unit_price'],\n",
128-
" revenue,\n",
129-
" int(stock_level),\n",
130-
" product['reorder_point'],\n",
131-
" product['supplier_lead_time']\n",
132-
" ])\n",
133-
"\n",
134-
"columns = [\n",
135-
"'date','product_id','product_name','category','warehouse',\n",
136-
"'units_sold','unit_price','revenue','stock_level',\n",
137-
"'reorder_point','supplier_lead_time']\n",
138-
"\n",
139-
"df = pd.DataFrame(data, columns=columns)\n",
140-
"\n",
141-
"df.head()"
142-
],
143-
"metadata": {
144-
"colab": {
145-
"base_uri": "https://localhost:8080/",
146-
"height": 206
147-
},
148-
"id": "hClHORy3_i0b",
149-
"outputId": "1bbe8c6a-ece8-4a5f-cb98-1ea6af5f95d8"
150-
},
89+
"metadata": {},
15190
"execution_count": null,
152-
"outputs": []
153-
},
154-
{
155-
"cell_type": "code",
91+
"outputs": [],
15692
"source": [
157-
"df.to_csv('inventory_transactions.csv', index=False)"
158-
],
159-
"metadata": {
160-
"id": "rp5-9QKg_wyx"
161-
},
162-
"execution_count": null,
163-
"outputs": []
93+
"# Generamos el dataset sintético usando el paquete:\n",
94+
"# - 50 productos × 3 bodegas × 365 días\n",
95+
"# - estacionalidad semanal y anual\n",
96+
"# - curva ABC (~20% de los SKUs concentran la mayoría de los ingresos)\n",
97+
"# - lead times log-normales\n",
98+
"# - stock evolucionado de forma coherente (con quiebres reales)\n",
99+
"params = GenerationParams(n_products=50, n_days=365, seed=42)\n",
100+
"df = generate_dataset(params)\n",
101+
"df.to_csv('inventory_transactions.csv', index=False)\n",
102+
"print(f'Generadas {len(df):,} filas')\n",
103+
"df.head()\n"
104+
]
164105
},
165106
{
166107
"cell_type": "markdown",

0 commit comments

Comments
 (0)