Este artículo presenta un desafío de SQL inspirado en la película "Los Sin Nombre" (无双), centrado en la simulación de la producción de billetes falsos. Se busca calcular la cantidad diaria de billetes que se pueden fabriccar y determinar el mtaerial más escaso para el día siguiente, basándose en los suministros diarios de papel especial, tinta que cambia de color y hilos de seguridad.
La trama gira en torno a un grupo de falsificadores que proveen cantidades variables de tres materiales clave cada día:
- Papel libre de ácido (en gramos)
- Tinta ópticamente variable (en miligramos)
- Hilos de seguridad
La fabricación de un billete falso requiere:
- 1 gramo de papel libre de ácido
- 0.005 gramos de tinta ópticamente variable
- 1 hilo de seguridad
Se asume que los materiales no utilizados se pueden almacenar para su uso posterior, y no hay pérdidas durante la producción.
Tabla de Suministros Diarios:
| Columna | Tipo de Dato | Descripción |
|---|---|---|
date |
string | Fecha del registro |
acid_free_paper_supply |
int | Suministro de papel libre de ácido (g) |
optically_variable_ink_supply |
int | Suministro de tinta ópticamente variable (mg) |
security_thread_supply |
int | Suministro de hilo de seguridad |
La producción de billetes está limitada por el material que se agota primero, similar a la "teoría del barril" (bottleneck theory). La solución implica:
- Calcular la producción máxima de billetes permitida por cada material de forma individual.
- Identificar el material que restringe la producción utilizando la función
LEAST. - Utilizar funciones de ventana para acumular los suministros y calcular la producción a lo largo del tiempo, considerando que los materiales no utilizados se conservan.
- Determinar el material más escaso para el día siguiente comparando la producción acumulada total con la producción permitida por cada material.
Se utilizan scripts en Python con las librerías NumPy y Pandas para generar datos de suministro diario aleatorios. Luego, estos datos se cargan en una tabla de Hive para su posterior análisis con SQL.
Código Python para Generación de Datos y Carga en Hive:
import numpy as np
import pandas as pd
import scipy.stats
from pyhive import hive
import os
# --- Constantes y Parámetros de Simulación ---
RANDOM_SEED = 2025
START_DATE = "2025-05-01"
NUM_DAYS = 10
TOTAL_CURRENCY_TARGET = 1_000_000
# Requisitos por billete
PAPER_PER_BILL = 1.0 # gramos
INK_PER_BILL = 5.0 # miligramos
THREAD_PER_BILL = 1.0 # unidades
# Consumo total estimado (con un margen de seguridad del 5%)
TOTAL_PAPER_NEEDED = PAPER_PER_BILL * TOTAL_CURRENCY_TARGET * 1.05
TOTAL_INK_NEEDED = INK_PER_BILL * TOTAL_CURRENCY_TARGET * 1.05
TOTAL_THREAD_NEEDED = THREAD_PER_BILL * TOTAL_CURRENCY_TARGET * 1.05
# --- Generación de Pesos de Suministro Aleatorios ---
WEIGHT_RANGE = (0.2, 2.0) # Rango para la distribución uniforme
paper_weights = scipy.stats.uniform.rvs(
loc=WEIGHT_RANGE[0], scale=WEIGHT_RANGE[1] - WEIGHT_RANGE[0],
size=NUM_DAYS, random_state=RANDOM_SEED - 1
)
ink_weights = scipy.stats.uniform.rvs(
loc=WEIGHT_RANGE[0], scale=WEIGHT_RANGE[1] - WEIGHT_RANGE[0],
size=NUM_DAYS, random_state=RANDOM_SEED
)
thread_weights = scipy.stats.uniform.rvs(
loc=WEIGHT_RANGE[0], scale=WEIGHT_RANGE[1] - WEIGHT_RANGE[0],
size=NUM_DAYS, random_state=RANDOM_SEED + 1
)
# Normalización de pesos para que sumen 1
paper_weights /= paper_weights.sum()
ink_weights /= ink_weights.sum()
thread_weights /= thread_weights.sum()
# --- Creación del DataFrame y Exportación a CSV ---
df_supply = pd.DataFrame({
"acid_free_paper_supply": (TOTAL_PAPER_NEEDED * paper_weights).round().astype(int),
"optically_variable_ink_supply": (TOTAL_INK_NEEDED * ink_weights).round().astype(int),
"security_thread_supply": (TOTAL_THREAD_NEEDED * thread_weights).round().astype(int)
})
df_supply["date"] = pd.date_range(start=START_DATE, periods=NUM_DAYS, freq="D")
# Guardar en CSV para cargar en Hive
output_csv_path = "./daily_supply_records.csv"
columns_to_export = ["date", "acid_free_paper_supply", "optically_variable_ink_supply", "security_thread_supply"]
df_supply[columns_to_export].to_csv(output_csv_path, header=False, index=False, encoding="utf-8-sig")
# --- Carga de Datos en Hive ---
# Configuración de conexión a Hive
hive_host = "127.0.0.1"
hive_port = 10000
hive_user = "your_username" # Reemplazar con tu usuario
try:
conn = hive.Connection(host=hive_host, port=hive_port, username=hive_user)
cursor = conn.cursor()
# Eliminar tabla si existe
drop_table_sql = "DROP TABLE IF EXISTS counterfeit_ops.daily_material_supply"
cursor.execute(drop_table_sql)
print(f"Executed: {drop_table_sql}")
# Crear tabla en Hive
create_table_sql = """
CREATE TABLE IF NOT EXISTS counterfeit_ops.daily_material_supply (
`record_date` STRING COMMENT 'Date',
paper_supply INT COMMENT 'Acid-free paper supply (g)',
ink_supply INT COMMENT 'Optically variable ink supply (mg)',
thread_supply INT COMMENT 'Security thread supply'
)
COMMENT 'Daily supply records for counterfeit operation'
ROW FORMAT DELIMITED FIELDS TERMINATED BY ','
STORED AS TEXTFILE
"""
cursor.execute(create_table_sql)
print(f"Executed: {create_table_sql}")
# Cargar datos desde el archivo CSV local
abs_csv_path = os.path.abspath(output_csv_path)
load_data_sql = f"LOAD DATA LOCAL INPATH '{abs_csv_path}' OVERWRITE INTO TABLE counterfeit_ops.daily_material_supply"
cursor.execute(load_data_sql)
print(f"Executed: {load_data_sql}")
cursor.close()
conn.close()
print("Data loaded into Hive successfully.")
except Exception as e:
print(f"Error during Hive operation: {e}")
El siguiente código SQL calcula la producción diaria de billetes y determina el material más escaso.
WITH MaterialProduction AS (
-- Calcula la producción máxima teórica por cada material de forma acumulada
SELECT
record_date,
paper_supply,
ink_supply,
thread_supply,
-- Producción posible solo con papel (1g por billete)
SUM(paper_supply) OVER (ORDER BY record_date ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_paper_production,
-- Producción posible solo con tinta (asumiendo 5mg por billete)
FLOOR(SUM(ink_supply) OVER (ORDER BY record_date ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) / 5.0) AS cumulative_ink_production,
-- Producción posible solo con hilo de seguridad (1 unidad por billete)
SUM(thread_supply) OVER (ORDER BY record_date ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_thread_production
FROM
counterfeit_ops.daily_material_supply
),
OverallRestriction AS (
-- Determina la producción máxima diaria total restringida por el material más escaso
SELECT
record_date,
paper_supply,
ink_supply,
thread_supply,
cumulative_paper_production,
cumulative_ink_production,
cumulative_thread_production,
-- El menor de los valores acumulados determina la producción total factible
LEAST(
cumulative_paper_production,
cumulative_ink_production,
cumulative_thread_production
) AS overall_production_limit
FROM
MaterialProduction
)
-- Cálculo final de la producción diaria y el material más escaso
SELECT
record_date,
cumulative_paper_production,
cumulative_ink_production,
cumulative_thread_production,
overall_production_limit,
-- Producción diaria: diferencia con la producción acumulada del día anterior
overall_production_limit - LAG(overall_production_limit, 1, 0) OVER (ORDER BY record_date ASC) AS daily_production_quantity,
-- Identifica el material más escaso si el objetivo de producción no se ha alcanzado
CASE
WHEN overall_production_limit >= 1000000 THEN NULL -- Objetivo de producción alcanzado
ELSE CONCAT_WS(',',
IF(cumulative_paper_production = overall_production_limit, 'Papel Libre de Ácido', NULL),
IF(cumulative_ink_production = overall_production_limit, 'Tinta Ópticamente Variable', NULL),
IF(cumulative_thread_production = overall_production_limit, 'Hilo de Seguridad', NULL)
)
END AS most_scarce_material
FROM
OverallRestriction
ORDER BY
record_date;
Resultados Simulados:
| record_date | cumulative_paper_production | cumulative_ink_production | cumulative_thread_production | overall_production_limit | daily_production_quantity | most_scarce_material |
|---|---|---|---|---|---|---|
| 2025-05-01 | 139293 | 36604 | 51579 | 36604 | 36604 | Tinta Ópticamente Variable |
| 2025-05-02 | 300720 | 184888 | 133386 | 133386 | 96782 | Hilo de Seguridad |
| 2025-05-03 | 360345 | 339815 | 303165 | 303165 | 169779 | Hilo de Seguridad |
| 2025-05-04 | 391211 | 422448 | 334383 | 334383 | 31218 | Hilo de Seguridad |
| 2025-05-05 | 454196 | 496570 | 426535 | 426535 | 92152 | Hilo de Seguridad |
| 2025-05-06 | 497465 | 551300 | 598018 | 497465 | 70930 | Papel Libre de Ácido |
| 2025-05-07 | 664497 | 665371 | 646287 | 646287 | 148822 | Hilo de Seguridad |
| 2025-05-08 | 821998 | 754988 | 805933 | 754988 | 108701 | Tinta Ópticamente Variable |
| 2025-05-09 | 938544 | 914610 | 910409 | 910409 | 155421 | Hilo de Seguridad |
| 2025-05-10 | 1050000 | 1050000 | 1050001 | 1050000 | 139591 | NULL |