Cómo analizar archivos CSV grandes con LLM locales en C# (Español (Spanish))

Cómo analizar archivos CSV grandes con LLM locales en C#

Thursday, 18 December 2025

//

18 minute read

Serie: LLM locales para los datos - Parte 1 de 2

Este es el error que todo el mundo comete: tratan de alimentar su CSV en un LLM. No. Los LLM deben generar consultas, no consumir datos.

Tienes un archivo CSV de 500MB y quieres preguntar "¿Cuál es el valor medio del pedido por región?" Copiloto en Excel puede hacer esto, pero ¿qué pasa si sus datos son demasiado sensibles para los servicios en la nube?

Este artículo muestra cómo - localmente, en privado, en C#.

Utilice DuckDB para consultar archivos CSV directamente. Utilice un LLM local para generar el SQL. El LLM nunca ve sus datos - sólo el esquema. Resultado: consultas sub-100ms en archivos de millones de filas, completamente fuera de línea.

Para un tratamiento complementario, más centrado en CLI que se expande en el uso de perfiles estadísticos como la interfaz LLM (y muestra una herramienta completa implementando estas ideas - perfilamiento, modo SQL seguro, clonación sintética y detección de deriva), vea el artículo de acompañamiento: DataSummarizer: Profiling rápido de datos locales - especialmente la sección "The Key Upgrade: Statistics as the Interface". Estos dos artículos forman una serie corta sobre prácticas LLM + patrones de consulta locales.

El punto de vista básico

El uso de un LLM como una tienda de datos es la abstracción equivocada. Los LLM son fundamentalmente incapaces de escanear millones de filas para calcular un promedio - eso no es para lo que están. Incluso una ventana de contexto token de 200K encaja tal vez 50.000 filas. Su CSV de 500MB tiene millones.

El patrón correcto: Razones de LLM, computaciones de base de datos.

flowchart LR
    A[User Question] --> B[LLM]
    B --> C[SQL Query]
    C --> D[DuckDB]
    D --> E[Results]
    
    style B stroke:#333,stroke-width:4px
    style D stroke:#333,stroke-width:4px

Observe lo que está sucediendo: el LLM genera una consulta SQL basada en su pregunta y el esquema. DuckDB lo ejecuta contra los datos reales. El LLM nunca toca sus datos - sólo ve nombres y tipos de columnas. Por eso es rápido, privado y preciso.

Para obtener más información sobre cómo tratar los perfiles como la interfaz LLM (y un CLI concreto que implementa la narración de perfil-primero, preguntas y respuestas seguras respaldadas por SQL, sesiones respaldadas por registro y clonación sintética), véase DataSummarizer: Profiling rápido de datos locales.

¿Por qué no simplemente cargarlo en la memoria?

Los enfoques obvios comparten el mismo defecto fatal:

CsvHelper / DataFrames: Cargar archivo entero en RAM. Un CSV de 500MB se convierte en 2-4GB de objetos. ¿Un archivo de 5GB?

SQLite / PostgreSQL: Requiere un paso de importación lento (minutos para archivos grandes), definiciones de esquemas iniciales y administración de bases de datos.

PandasAI: Todavía carga todo en la memoria. Además, ejecutar código arbitrario generado por LLM es una pesadilla de seguridad - SQL es declarativo y sandboxable; Python no lo es.

¿Por qué DuckDB

DuckDB es diferente. Consulta archivos CSV directamente - ningún paso de importación, ninguna carga en la memoria:

using var connection = new DuckDBConnection("DataSource=:memory:");
connection.Open();
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT Region, SUM(Amount) FROM 'sales.csv' GROUP BY Region";
// Executes directly against the file - no import, no memory explosion

La característica del asesino: trata los archivos como tablas. Apuntalo a un archivo CSV, Parquet o JSON y consulta de inmediato. No CREAR TABLE, no insertar a granel, no esperar.

Factor CsvHelper SQLite DuckDB |--------|-----------|--------|--------| | Memoria Carga archivos enteros Cargas durante la importación Corrientes desde el disco | Configuración # Ninguno # # Esquema + importación # # Ninguno # | Archivo 500MB ~2GB RAM Minutos para importar Instantánea | Archivo 5GB # Muy lento # # Obras finas # | Parquet No No Sí (10-100x más rápido)

DuckDB es lo que los ingenieros de datos utilizan en Python exactamente para este caso de uso. Encuadernaciones .NET darle soporte completo ADO.NET - se siente como cualquier otra base de datos, excepto que usted está consultando archivos.

La pila

¿Por qué éste? |-----------|--------------| | DuckDB Pregunta CSV directamente, ningún paso de importación | DuckDB.NET Apoyo completo ADO.NET, se siente nativo | Ollama Inferencia local, sin claves API, sin nube | Bogus Datos de prueba realistas a cualquier escala | qwen2.5-coder:7b Mejor precisión SQL en tamaño 7B

Nota sobre la seguridad: Estamos ejecutando SQL generado por LLM. Esto es más seguro que el código arbitrario, pero todavía requiere validación. Sección de seguridad Para una discusión más amplia sobre convertir perfiles en la interfaz LLM y un CLI que implementa estos patrones, vea la pieza de acompañamiento DataSummarizer: Profiling rápido de datos locales.

Configuración del proyecto

Vamos a crear un proyecto de ejemplo. Instale los paquetes NuGet:

dotnet add package DuckDB.NET.Data.Full
dotnet add package OllamaSharp
dotnet add package Bogus

Tire de un modelo centrado en la codificación que es bueno en SQL:

ollama pull qwen2.5-coder:7b

La arquitectura

Así es como encajan las piezas:

flowchart TB
    subgraph Input
        Q[User Question]
        CSV[CSV File]
    end
    
    subgraph Processing
        Schema[Extract Schema]
        Sample[Get Sample Rows]
        Context[Build LLM Context]
        LLM[Generate SQL]
        Validate[Validate SQL]
        Execute[Execute Query]
    end
    
    subgraph Output
        Results[Query Results]
    end
    
    CSV --> Schema
    CSV --> Sample
    Schema --> Context
    Sample --> Context
    Q --> Context
    Context --> LLM
    LLM --> Validate
    Validate -->|Error| LLM
    Validate -->|OK| Execute
    CSV --> Execute
    Execute --> Results
    
    style LLM stroke:#333,stroke-width:4px
    style Execute stroke:#333,stroke-width:4px

La perspicacia clave: damos el LLM esquema y datos de muestra, no los datos reales. Esto mantiene el contexto pequeño y las respuestas rápidas.

¿Por qué importa esto?: El LLM genera intent (SQL). DuckDB lo ejecuta. El paso de validación captura errores de sintaxis antes de la ejecución. El bucle de reintent maneja el error ocasional. Esta separación es lo que hace que el sistema sea seguro y preciso. Para una referencia más detallada, centrada en CLI (incluyendo la narración de perfil-primero y los límites de ejecución segura SQL) vea DataSummarizer: Profiling rápido de datos locales.

Paso 1: Generar datos de prueba con Bogus

¿Ya tiene datos CSV? Saltar a Paso 2: Construir el contexto del esquema.

Antes de que podamos probar nuestro analizador CSV de LLM, necesitamos datos para analizar. Para el desarrollo y las pruebas, los datos sintéticos son mejores que los datos reales:

  1. Ensayos de escala - Generar filas de 100K, 1M o 10M para verificar el rendimiento en diferentes tamaños
  2. Privacidad - No hay riesgo de exponer datos reales de clientes/negocios en demos o capturas de pantalla
  3. Reproducibilidad - Misma semilla = los mismos datos, haciendo que los errores reproducibles
  4. Cajas de borde - Controlar la distribución (por ejemplo, fuerza de retorno 5%, rangos de fechas específicos)

¿Qué es Bogus?

Bogus es un puerto .NET de la biblioteca popular faker.js. Genera datos falsos de aspecto realista - nombres, direcciones, correos electrónicos, fechas, números - con el apoyo local adecuado. En lugar de hacer pruebas manuales de archivos CSV o el uso de datos de basura aleatorios, Bogus le da datos que Mira. real:

  • f.Name.FullName() → "John Smith" (no "asdf1234")
  • f.Internet.Email() → "[email protected]" (correctamente formateado)
  • f.Date.Between(start, end) → Distribución realista de la fecha
  • f.Commerce.ProductName() → "Queso de granito artesanal" (divertido, pero reconocible)

Esto importa porque los datos realistas te ayudan a detectar problemas -formateo raro, agregados inesperados, casos de borde en el manejo de la fecha - que las cadenas aleatorias ocultarían.

Definir el modelo de datos

internal class SaleRecord
{
    public string OrderId { get; set; } = "";
    public DateTime OrderDate { get; set; }
    public string CustomerId { get; set; } = "";
    public string CustomerName { get; set; } = "";
    public string Region { get; set; } = "";
    public string Category { get; set; } = "";
    public string ProductName { get; set; } = "";
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
    public decimal Discount { get; set; }
    public bool IsReturned { get; set; }
}

Configure el Faker

Bogus utiliza una API fluida para definir reglas de generación:

var categories = new[] { "Electronics", "Clothing", "Home & Garden", "Sports", "Books" };
var regions = new[] { "North", "South", "East", "West", "Central" };

var faker = new Faker<SaleRecord>()
    .RuleFor(s => s.OrderId, f => f.Random.Guid().ToString()[..8].ToUpper())
    .RuleFor(s => s.OrderDate, f => f.Date.Between(
        new DateTime(2022, 1, 1), 
        new DateTime(2024, 12, 31)))
    .RuleFor(s => s.CustomerId, f => $"CUST-{f.Random.Number(10000, 99999)}")
    .RuleFor(s => s.CustomerName, f => f.Name.FullName())
    .RuleFor(s => s.Region, f => f.PickRandom(regions))
    .RuleFor(s => s.Category, f => f.PickRandom(categories))
    .RuleFor(s => s.ProductName, (f, s) => GenerateProductName(f, s.Category))
    .RuleFor(s => s.Quantity, f => f.Random.Number(1, 20))
    .RuleFor(s => s.UnitPrice, f => f.Random.Decimal(9.99m, 299.99m))
    .RuleFor(s => s.Discount, f => f.Random.Bool(0.3f) ? f.Random.Decimal(0.05m, 0.25m) : 0m)
    .RuleFor(s => s.IsReturned, f => f.Random.Bool(0.05f));

Vamos a romper lo que está pasando:

  • f (Faker) - La instancia generadora con acceso a todos los módulos de datos (Nombre, Fecha, Aleatorio, etc.)
  • f.Random.Guid().ToString()[..8] - Generar un GUID pero tomar sólo los primeros 8 caracteres para una identificación de orden legible
  • f.Date.Between() - Fecha aleatoria dentro de un rango realista (no año 9999)
  • f.PickRandom(array) - Seleccione aleatoriamente entre opciones predefinidas (asegura categorías válidas)
  • f.Random.Bool(0.3f) - 30% de probabilidad de verdad (el 30% de los pedidos obtienen un descuento)
  • (f, s) sintaxis - Acceda tanto al falsificador como al disco parcialmente construido. ProductName depende de Category

Los (f, s) patrón es potente - significa "Electronics" pedidos obtener nombres de productos electrónicos, no elementos aleatorios. Esta coherencia hace que los datos generados mucho más realista para las pruebas de agregación como "ingreso por categoría".

Generar y escribir en CSV

var records = faker.Generate(100_000); // Adjust for your testing needs

await using var writer = new StreamWriter(csvPath, false, Encoding.UTF8);
await writer.WriteLineAsync("OrderId,OrderDate,CustomerId,CustomerName,Region,Category,...");

foreach (var record in records)
{
    var total = record.Quantity * record.UnitPrice * (1 - record.Discount);
    await writer.WriteLineAsync($"{record.OrderId},{record.OrderDate:yyyy-MM-dd},...");
}

Las filas de 100K generan alrededor de 15MB de CSV - lo suficiente para probar con, pero puede escalar fácilmente a millones. La generación es rápida (~2 segundos para las filas de 100K) porque Bogus está optimizado para la generación a granel.

Consejo: Set Randomizer.Seed = new Random(12345) antes de generar para obtener datos reproducibles. Misma semilla = los mismos registros "aleatorios" cada vez, lo cual es invaluable para la depuración.

Paso 2: Construir el contexto del esquema

Antes de que el LLM pueda generar SQL, necesita entender la estructura de datos. Extraemos esto de DuckDB:

El modelo de contexto

public class DataContext
{
    public string CsvPath { get; set; } = "";
    public List<ColumnInfo> Columns { get; set; } = new();
    public List<Dictionary<string, string>> SampleRows { get; set; } = new();
    public long RowCount { get; set; }
}

public class ColumnInfo
{
    public string Name { get; set; } = "";
    public string Type { get; set; } = "";  // VARCHAR, DOUBLE, TIMESTAMP, etc.
}

Esto captura todo lo que el LLM necesita: nombres de columna, tipos y algunas filas de muestra para entender el formato de datos.

Extraer el esquema

DuckDB puede describir cualquier CSV sin cargarlo todo:

private DataContext BuildContext(DuckDBConnection connection, string csvPath)
{
    var context = new DataContext { CsvPath = csvPath };

    // Get schema - DuckDB infers types from the CSV
    using var cmd = connection.CreateCommand();
    cmd.CommandText = $"DESCRIBE SELECT * FROM '{csvPath}'";
    using var reader = cmd.ExecuteReader();

    while (reader.Read())
    {
        context.Columns.Add(new ColumnInfo
        {
            Name = reader.GetString(0),  // Column name
            Type = reader.GetString(1)   // Inferred type
        });
    }

    return context;
}

Los DESCRIBE comando lee sólo el encabezado del archivo más unas pocas filas para la inferencia de tipo - es instantáneo incluso en archivos enormes.

Obtención de datos de la muestra

Las filas de muestra ayudan al LLM a entender los formatos de datos (fechas, ID, etc.):

using var cmd = connection.CreateCommand();
cmd.CommandText = $"SELECT * FROM '{csvPath}' LIMIT 3";
using var reader = cmd.ExecuteReader();

while (reader.Read())
{
    var row = new Dictionary<string, string>();
    for (int i = 0; i < reader.FieldCount; i++)
    {
        var value = reader.IsDBNull(i) ? "NULL" : reader.GetValue(i)?.ToString() ?? "";
        row[reader.GetName(i)] = value;
    }
    context.SampleRows.Add(row);
}

Tres filas suele ser suficiente - muestra el LLM qué formatos esperar sin desperdiciar tokens.

Paso 3: Generar SQL con el LLM

Esta es la parte difícil. La ingeniería rápida aquí es no negociable - sin reglas estrictas, los LLM locales producirán SQL creativo pero roto. El objetivo es el determinismo, no la creatividad.

Construyendo el Prompt

private string BuildPrompt(DataContext context, string question, string? previousError)
{
    var sb = new StringBuilder();

    sb.AppendLine("You are a SQL expert. Generate a DuckDB SQL query to answer the user's question.");
    sb.AppendLine();
    sb.AppendLine("IMPORTANT RULES:");
    sb.AppendLine("1. The table is accessed directly from the CSV file path");
    sb.AppendLine("2. Use single quotes around the file path in FROM clause");
    sb.AppendLine("3. DuckDB syntax - use LIMIT not TOP, use || for string concat");
    sb.AppendLine("4. Return ONLY the SQL query, no explanation, no markdown");
    sb.AppendLine();
    
    sb.AppendLine($"CSV File: '{context.CsvPath}'");
    sb.AppendLine($"Row Count: {context.RowCount:N0}");
    sb.AppendLine();
    
    // Schema
    sb.AppendLine("Schema:");
    foreach (var col in context.Columns)
    {
        sb.AppendLine($"  - {col.Name}: {col.Type}");
    }

La sección de reglas es crucial - le dice al LLM exactamente cómo formatear la consulta para DuckDB. Ser explícito acerca de la sintaxis (LIMIT vs TOP, cadena de concatenación) previene errores comunes.

Añadir datos de la muestra

    if (context.SampleRows.Count > 0)
    {
        sb.AppendLine();
        sb.AppendLine("Sample data (first 3 rows):");
        foreach (var row in context.SampleRows)
        {
            var values = row.Select(kv => $"{kv.Key}='{kv.Value}'");
            sb.AppendLine($"  {{{string.Join(", ", values)}}}");
        }
    }

Recuperación de errores

Si un intento anterior falló, incluya el error:

    if (previousError != null)
    {
        sb.AppendLine();
        sb.AppendLine("YOUR PREVIOUS QUERY HAD AN ERROR:");
        sb.AppendLine(previousError);
        sb.AppendLine("Please fix the query based on this error.");
    }

    sb.AppendLine();
    sb.AppendLine($"Question: {question}");
    sb.AppendLine();
    sb.AppendLine("SQL Query (no markdown, no explanation):");

    return sb.ToString();
}

Este mecanismo de reintentar es importante - los LLM locales a veces cometen errores de sintaxis, y dándoles el error generalmente lo corrige en el segundo intento.

Llamando al LLM

var request = new GenerateRequest { Model = _model, Prompt = prompt };
var response = await _ollama.GenerateAsync(request).StreamToEndAsync();
var sql = CleanSqlResponse(response?.Response ?? "");

Los StreamToEndAsync() espera la respuesta completa. Para una mejor UX, usted podría transmitir tokens a medida que llegan.

Limpiando la respuesta

Los LLM a menudo envuelven SQL en bloques de código Markdown a pesar de que se les dijo que no:

private string CleanSqlResponse(string response)
{
    var sql = response.Trim();

    // Remove markdown code blocks if present
    if (sql.StartsWith("```"))
    {
        var lines = sql.Split('\n').ToList();
        lines.RemoveAt(0);  // Remove opening ```sql
        if (lines.Count > 0 && lines[^1].Trim().StartsWith("```"))
        {
            lines.RemoveAt(lines.Count - 1);  // Remove closing ```
        }
        sql = string.Join('\n', lines);
    }

    return sql.Trim('`', ' ', '\n', '\r');
}

Paso 4: Validar antes de ejecutar

DuckDB's EXPLAIN nos permite comprobar la sintaxis SQL sin ejecutar la consulta:

private string? ValidateSql(DuckDBConnection connection, string sql)
{
    try
    {
        using var cmd = connection.CreateCommand();
        cmd.CommandText = $"EXPLAIN {sql}";
        cmd.ExecuteNonQuery();
        return null; // Valid
    }
    catch (Exception ex)
    {
        return ex.Message;
    }
}

Si la validación falla, se devuelve el error al LLM y se reintenta (hasta un límite).

Paso 5: Ejecutar y Formato de Resultados

Finalmente, ejecute la consulta y formatee la salida:

private QueryResult ExecuteQuery(DuckDBConnection connection, string sql)
{
    var result = new QueryResult { Sql = sql };

    try
    {
        using var cmd = connection.CreateCommand();
        cmd.CommandText = sql;
        using var reader = cmd.ExecuteReader();

        // Capture column names
        for (int i = 0; i < reader.FieldCount; i++)
        {
            result.Columns.Add(reader.GetName(i));
        }

        // Capture rows
        while (reader.Read())
        {
            var row = new List<object?>();
            for (int i = 0; i < reader.FieldCount; i++)
            {
                row.Add(reader.IsDBNull(i) ? null : reader.GetValue(i));
            }
            result.Rows.Add(row);
        }

        result.Success = true;
    }
    catch (Exception ex)
    {
        result.Success = false;
        result.Error = ex.Message;
    }

    return result;
}

Los QueryResult clase (se muestra en su totalidad en el proyecto de la muestra) incluye un ToString() método que formatee los resultados como una tabla legible.

Añadiendo contexto de conversación

Para el análisis interactivo, los usuarios a menudo quieren hacer preguntas de seguimiento:

"What's the total revenue?"
→ "Break that down by region"
→ "Show the top 5 regions"

Las preguntas segunda y tercera sólo tienen sentido con el contexto de la primera.

Seguimiento de la historia de la conversación

public class ConversationTurn
{
    public string Question { get; set; } = "";
    public string Sql { get; set; } = "";
    public bool Success { get; set; }
    public int RowCount { get; set; }
    public string Summary { get; set; } = "";  // "Single value: 1234567.89"
}

Incluyendo Historia en Prompts

if (_history.Count > 0)
{
    sb.AppendLine();
    sb.AppendLine("CONVERSATION HISTORY (for context):");
    
    foreach (var turn in _history.TakeLast(5))  // Last 5 turns
    {
        sb.AppendLine($"Q: {turn.Question}");
        sb.AppendLine($"SQL: {turn.Sql}");
        if (turn.Success)
        {
            sb.AppendLine($"Result: {turn.Summary}");
        }
        sb.AppendLine();
    }
}

La historia le da al contexto LLM para entender referencias como "eso", "estos resultados", o "descomponerlo aún más".

¿Qué modelo usar?

Para la generación SQL, los modelos centrados en la codificación funcionan mejor. Estas son las opciones disponibles a través de Biblioteca modelo de Ollama:

Modelo # Tamaño # # Velocidad # # Calidad # # Enlace

------- ------ ------- --------- ------
deepseek-coder-v2:16b 9GB Medio Mejor Ollama
codellama:7b # 4GB # # Rápido # # Bueno # Ollama
llama3.2:3b 2GB Muy rápido Aceptable Ollama

Para la mayoría de los casos de uso, qwen2.5-coder:7b golpea el punto dulce - SQL precisa, buena velocidad, se ejecuta en hardware modesto (8GB + RAM).

Desempeño

Parámetros de referencia del mundo real

Ensayo en un archivo CSV de 100K fila (14MB) en una máquina de desarrollo estándar (Ryzen 5, NVMe SSD, 32GB RAM):

Tipo de consulta Tiempo |------------|------|

Simple cuenta # # 65ms

GRUPO CON SUM 58-68ms Agregación compleja con FILTER 63ms GROUP DE Mesas Múltiples por con TENER 71ms

Sub-100ms para consultas analíticas en filas de 100K - sin ningún paso de importación. La complejidad de la consulta importa más que el conteo de filas; el motor columnar de DuckDB maneja las agregación de manera eficiente independientemente del tamaño del archivo.

Convertir archivos grandes a Parquet

Para archivos de más de 1 GB, Formato Parquet es 10-100x más rápido:

using var cmd = connection.CreateCommand();
cmd.CommandText = $"COPY (SELECT * FROM '{csvPath}') TO '{parquetPath}' (FORMAT PARQUET)";
cmd.ExecuteNonQuery();

El archivo comprimido Parquet también es mucho más pequeño.

Consideraciones relativas a la seguridad

Al ejecutar SQL generado por LLM:

private bool IsSafeQuery(string sql)
{
    var dangerous = new[] { "DROP", "DELETE", "TRUNCATE", "UPDATE", "INSERT", "ALTER", "CREATE" };
    var upperSql = sql.ToUpperInvariant();
    return !dangerous.Any(d => upperSql.Contains(d));
}

El modo en memoria de DuckDB también proporciona aislamiento natural - no puede afectar a sus bases de datos de producción.

Ejemplo completo

Así es como todo se une:

// Generate test data
await GenerateSalesCsvAsync("sales.csv", 100_000);

// Simple query
using var service = new CsvQueryService("qwen2.5-coder:7b", verbose: true);
var result = await service.QueryAsync("sales.csv", "What are total sales by region?");
Console.WriteLine(result);

// Conversational analysis
using var analyser = new ConversationalCsvAnalyser("sales.csv", "qwen2.5-coder:7b");

Console.WriteLine(await analyser.AskAsync("What's the total revenue?"));
Console.WriteLine(await analyser.AskAsync("Break that down by category"));
Console.WriteLine(await analyser.AskAsync("Which category has the most returns?"));

Resumen

El modelo mental para mantener: LLMs razón; bases de datos computar.

No alimente datos al LLM. Feed it schema, deje que genere SQL, ejecute ese SQL contra un motor de consulta adecuado. Esta separación es la razón por la que el enfoque funciona a escala.

La aplicación:

  1. DuckDB consultas CSV directamente - no importa, flujos desde el disco
  2. Esquema + muestras dar el contexto LLM suficiente sin exponer datos
  3. Normas de rapidez estrictas fuerza determinista SQL, no la prosa creativa
  4. Validación a través de EXPLAIN captura de errores antes de la ejecución
  5. Reintentar con retroalimentación de error maneja el deslice de sintaxis ocasional

El resultado: consultas analíticas sub-100ms en archivos de millones de filas, completamente fuera de línea, con datos que nunca salen de su máquina.

El proyecto muestral completo está disponible en: La mayoría de lucid.CsvLlm - incluye CsvQueryService, ConversationalCsvAnalyser, y la generación de datos basada en Bogus.

Recursos

DuckDB

Ollama & LLMs

Generación de datos de ensayo

Alternativas mencionadas

Artículos relacionados

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.