Simulación de Producción de Billetes Falsos con SQL: Análisis de Suministro de Materiales

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:

  1. Calcular la producción máxima de billetes permitida por cada material de forma individual.
  2. Identificar el material que restringe la producción utilizando la función LEAST.
  3. 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.
  4. 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

Etiquetas: SQL hive window functions data analysis Simulation

Publicado el 10-2 14:56