Як аналізувати великі файли CSV з локальними LLM у C# (Українська (Ukrainian))

Як аналізувати великі файли CSV з локальними LLM у C#

Thursday, 18 December 2025

//

17 minute read

Серія: локальні LLM для даних - Частина 1 з 2

Ось помилка, яку всі роблять: вони намагаються погодувати свої CSV на LLM. LLM має створювати запити, а не споживати дані.

У вас є файл на 500 МБ CSV і ви спитаєте: "Який середній порядок за областю?" Інструменти на зразок Copilot in Excel Можна зробити це, але що, коли ваші дані занадто чутливі до послуг хмар?

Ця стаття покаже вам, як - локально, приватно, у C#.

Скористайтеся DuckDB для безпосереднього опитування файлів CSV. Скористайтеся локальним LLM для створення SQL. LLM ніколи не бачитиме ваших даних - лише схему. Результат: запити під100 мс на файлах мільйонів рядків повністю поза мережею.

Для додаткового, більш фокусованого на CLI лікування, що розширює використання статистичних профілів за допомогою інтерфейсу LLM (і показує повноцінний інструмент, який реалізує ці ідеї - профілювання, безпечний режим SQL, штучне клонування і визначення дрифів), дивіться супровідну статтю: DataSummarizer: швидке профілювання локальних даних - особливо розділ " Оновити ключ: Статистика як Інтерфейс ." Ці дві статті утворюють короткий ряд з практичними локальними шаблонами запитів LLM +.

Основна проникливість

Використання LLM як сховища даних є неправильною абстракцією. LLM є фундаментально нездатним для сканування мільйонів рядків для обчислення середнього значення - це не те, що вони означають. Навіть контекстне вікно 200K містить, можливо, 50 000 рядків. У 500МБ CSV є мільйони.

Правильний зразок: Причина LLM, база даних обчислюється.

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

Зауважте, що відбувається: LLM створює запит SQL на основі вашого питання і схеми. DuckDB виконує це завдання на основі справжніх даних. LLM ніколи не торкається ваших даних - він бачить лише назви стовпчиків і типи. Ось чому він швидкий, приватний і точний.

Докладніше про те, як поводитися з профілями, як з інтерфейсом LLM (і бетонним CLI, який реалізує перший запис профілю, безпечний SQL- зі зворотною зоною Q&A, сеанси зі зворотним реєстром і штучне клонування), див. DataSummarizer: швидке профілювання локальних даних.

Чому б не завантажити його в пам'ять?

Очевидні підходи до всіх мають однаковий фатальний недолік:

CsvHelper / DataFrames: Завантажити весь файл до RAM. 500MБ CSV перетвориться на 2- 4 ГБ об' єктів. Файл 5GB? Збій OOM.

SQLite / PostgreSQL: Вимагає повільного імпорту (у хвилинах великих файлів), визначення схеми на передньому плані і керування базами даних над головою.

PandasAI: Завантажує все до пам' яті. Крім того, виконання команди LLM- створеного довільного коду є кошмаром безпеки - SQL є декартовим і пісочницею; Python - ні.

Why DuckDB

DuckDB є іншим. Запит до файлів CSV напряму - без кроку імпортування, без завантаження у пам' ять:

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

Функціональна можливість: програма вважатиме файли таблицями. Вкажіть на файли CSV, Parquet або JSON і негайно запитуйте. Без CREATE TABLE, без вставки маски, без очікування.

Дзвін-АліпольдCity in Hawaii USA (optional, probably does not need a translation) |--------|-----------|--------|--------| | Пам' ять ♫ Завантажує весь файл } Завантажується під час імпорту дрімає з диска | Налаштування ♪ None ♪ + import ♪ None' ♪ | 500 Мб файл }2GB RAM} } Щоб імпортувати зараз'ю | 5GB файл Дзвінок дзвону д'ІІІІІІІ | Parquet ♪ Ні ♪ ♪ Ні ♪ Так.

ДакDB - це те, що в Python використовують інженери даних для саме цього випадку використання. Прив' язка. NET Надає вам повну підтримку ADO. NET - це як і будь- яка інша база даних, але ви надсилаєте запит до файлів.

Стек

♪Of this One's Operation ♪ |-----------|--------------| | DuckDB безпосередньо, без кроку імпортування@ info: whatsthis | DuckDB. NET ♪ And othernet support, feels creat ♪ | Ольямаjapan. kgm Локальна, без клавіш API, без хмар | Богус Пряма перевірка даних на будь-якій шкалі | qwen2.5-coder:7b Точність SQL при 7Б розміру ♪

Примітка щодо безпекиМи виконуємо LLM- створений SQL. Це безпечніше за довільний код, але все ще потребує перевірки. Розділ Безпека для захисту. Для ширшого обговорення перетворення профілів на інтерфейс LLM і CLI, який реалізує ці шаблони, прочитайте супровідний елемент DataSummarizer: швидке профілювання локальних даних.

Налаштування проекту

Давайте створимо прикладний проект. Встановіть пакунки NuGet:

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

Перейдіть на модель, зосереджену на кодуванні, яка добре працює на SQL:

ollama pull qwen2.5-coder:7b

Архітектура

Ось як ці частинки поєднуються між собою:

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

Ключове розуміння: ми даємо LLM schema і дані вибіркиНе фактичні дані, це робить контекст маленьким і швидко реагує.

Чому це важливо?: Спроба створення LLM (SQL). DuckDB виконує цю дію. Крок перевірки виявить синтаксичні помилки перед виконанням. Повторний цикл виконує випадкову помилку. Саме це і робить систему безпечною і точною. Докладніші відомості щодо центру CLI (разом з першою розповіддю профілювання і безпечними обмеженнями на виконання SQL) див. DataSummarizer: швидке профілювання локальних даних.

Крок 1: Створити тестові дані за допомогою Бога

У вас вже є дані CSV? Перейти до Крок 2: Створити контекст схеми.

Перш ніж ми зможемо перевірити наш потужний аналізатор CSV, нам потрібні дані для аналізу. Для розробки і тестування, штучні дані б'ються з справжніми даними:

  1. Тестування масштабів - Створіть 100K, 1M або 10M рядків для перевірки швидкодії різних розмірів
  2. Конфіденційність - Не існує ризику виявлення реальних даних клієнта/ бізнесу у демонстраціях або знімках екрана
  3. Репродукція - Те саме насіння = однакові дані, що робить вади відшкодованими
  4. Регістри ребер - Керуйте розподілом (наприклад, сила 5% повертає, певний діапазон дат)

Що таке Бог?

Богус є портом . NET популярної бібліотеки підробок.js. Програма створює справжні фальшиві дані - імена, адреси, адреси, дати, цифри - з належною підтримкою локалі. Замість створення тестових файлів CSV або випадкових даних смітника, Bogus надає вам дані про те, що look real:

  • f.Name.FullName() → " John Smith " (не " asdf1234 ")
  • f.Internet.Email() → " john. cmith@ gmail. com " (точно форматований)
  • f.Date.Between(start, end) → Реалістичний розподіл дат
  • f.Commerce.ProductName() → "Значений гранітний сир" (фун, але придатний для розпізнавання)

Це важливо тому, що реалістичні дані допомагають вам помітити проблеми - дивне форматування, несподівані агресії, крайні випадки в роботі з датами - що випадкові рядки будуть ховатись.

Визначити модель даних

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; }
}

Налаштувати підробку

Bogus використовує API для визначення правил створення:

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));

Давайте розіб'ємо все, що відбувається:

  • f (Факер) - Генератор з доступом до всіх модулів даних (Назва, Дата, Випадковий тощо)
  • f.Random.Guid().ToString()[..8] - Створити GUID, але взяти лише перші 8 символів для придатного для читання ідентифікатора порядку
  • f.Date.Between() - Випадкова дата у реальному діапазоні (не 999 рік)
  • f.PickRandom(array) - Вибирає випадковий вибір з попередньо визначених параметрів (забезпечує коректні категорії)
  • f.Random.Bool(0.3f) - 30% шансу на істину (30% наказів отримують знижку)
  • (f, s) синтаксис - Доступ до обох підробок і частково вбудованого рекорду. ProductName залежить від Category

The (f, s) pattern є потужним, він означає " Eelectronics " команди отримують електронні назви продуктів, а не випадкові елементи. Ця послідовність робить створені дані більш реалістичними для перевірки агрегації на зразок " revenue за категорією ."

Створення і запис до 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},...");
}

100K- рядки створюють близько 15МБ CSV - достатньо, щоб перевірити їх, але ви можете просто змінити масштаб до мільйонів. Покоління є швидким (~2 секунди для 100K рядків), оскільки Боґус оптимізовано для створення великої кількості.

Підказка: Набір Randomizer.Seed = new Random(12345) перед створенням даних, які можна відтворити. Те саме насіння = однакові "випадкові" записується кожного разу, що є безцінним для усування вад.

Крок 2: Створити контекст схеми

Перш ніж LLM зможе створити SQL, він повинен зрозуміти структуру даних. Ми виберемо це з DuckDB:

Модель контексту

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.
}

За допомогою цього пункту можна зафіксувати все, що потрібно для LLM: назви стовпчиків, типи і декілька рядків для розуміння формату даних.

Видобування схеми

DuckDB може описувати будь- який CSV без завантаження всього цього:

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;
}

The DESCRIBE команда читає лише заголовок файла плюс декілька рядків для визначення типу - це миттєво навіть у великих файлах.

Отримання даних зразку

Зразки рядків допоможуть вам зрозуміти формати даних LLM (дати, ІД тощо):

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);
}

Зазвичай, достатньо трьох рядків - програма покаже формати LLM, які слід очікувати, не марнуючи жетонів.

Крок 3: Створити SQL за допомогою LLM

Це складна частина. Імпульсивна інженерія тут не обговорюється, без строгих правил, локальні LLM будуть створювати креативні, але зламані SQL. Метою є детермінізм, а не креативність.

Збудування жагу до виконання

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}");
    }

Розділ правил має вирішальне значення - він повідомляє LLM точно про те, як форматувати запит на DuckDB. Якщо ви детально розповідаєте про синтаксис (LIME vs TOP, string- з' єднання), то не зможете уникнути типових помилок.

Додавання даних зразку

    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)}}}");
        }
    }

Помилка під час відновлення

Якщо попередня спроба зазнала невдачі, додайте таку помилку:

    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();
}

Цей механізм повторення важливий - локальні LLM іноді роблять синтаксичні помилки, і надаючи їм помилки, як правило, виправляє їх при другій спробі.

Виклик LLM

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

The StreamToEndAsync() Чекає на відповідь повністю. Для кращого UX ви можете отримувати дані по потоці.

Очищення реакції

LLM часто перегортати SQL у блоки коду markdown, незважаючи на те, що не було вказано:

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');
}

Крок 4: Перевірте перед виконанням

DuckDB's EXPLAIN Надає нам змогу перевірити синтаксис SQL без виконання запиту:

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;
    }
}

Якщо спроба перевірки завершилася невдало, ми повертаємо помилку до LLM і повторимо спробу (до межі).

Крок 5. Результати виконання і форматування

Нарешті, виконайте запит і змініть вивід:

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;
}

The QueryResult клас (показано повністю у проекті прикладу) включає a ToString() метод, який задає формат, є придатним для читання таблицею.

Додавання контексту розмови

Для інтерактивного аналізу, користувачі часто бажають задавати наступні запитання:

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

Друге і третє питання має сенс лише з першого.

Журнал стеження за розмовами

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"
}

Включення історії в запити

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();
    }
}

Історія надає контексту LLM зрозуміти посилання на "це," "ці результати" або "знищити його."

Яку модель використовувати?

Для створення SQL моделі кодування найкраще працюватимуть. Ось перелік параметрів, які можна обрати за допомогою Модель бібліотеки Ollama:

Д-р Цукер: "Точно, що ми маємо на увазі?"

------- ------ ------- --------- ------
deepseek-coder-v2:16b Д. д. д. д. д. д. д. д. д. д. д. д. д. д. Ольямаjapan. kgm
codellama:7b ♪ 4GB' shame ♪ Ольямаjapan. kgm
llama3.2:3b ♪ 2GB} Дуже швидко Ольямаjapan. kgm

Для більшості випадків використання, qwen2.5-coder:7b виходить на солодке місце - точний SQL, непогана швидкість, працює на малому обладнанні (8 ГБ+ ОЗП).

Швидкодія

Просторові Benchmarks

Тестування на 100K рядок CSV файл (14MB) на стандартному комп' ютері dev (Ryzen 5, NVME SSD, 32GB RAM):

motion TIFF}Час_ часу@ title: window |------------|------| Точно} 65 мс* ГРУПІ З СУПЕ 58 - 68 мс aggregation with FILTER} 63ms ♪ Mi-table GROUP # з HAVING * 71 мс]

Під100 мм для аналітичних запитів у 100K рядках - без будь- якого кроку імпортування. Складність запиту є важливішим за кількість рядків; стовпчиковий рушій DuckDB ефективно керує агрегаціями, незалежно від розміру файлів.

Перетворити великі файли на паркет

Для файлів понад 1GB, Формат Parquet швидкості 10-100x:

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

Стиснений файл Parquet також набагато менший.

Обговорення безпеки

Під час виконання LLM- створеного SQL:

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));
}

Режим пам'яті ДакБ також надає природну ізоляцію - він не може вплинути на ваші виробничі бази даних.

Повний приклад

Вот как все объединяется.

// 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?"));

Зведення

Психічна модель: Причина LLM; бази даних обчислює.

Не надсилайте дані до LLM. Задайте схему, нехай створить SQL, виконайте цю команду SQL за допомогою відповідного рушія запитів. Ось чому такий підхід працює на шкалі.

Реалізація:

  1. DuckDB Запити до CSV напряму - немає імпорту, потоків з диска
  2. Схема + зразки надати LLM достатньо контексту без відкриття даних
  3. Суворі правила запитів примусова детермінована SQL, а не творча проза
  4. Перевірка через EXPLAIN виявити помилки перед виконанням
  5. Повторити спробу з відгуком на помилку керує випадковим вставленням синтаксису

Результат: під100- мс аналітичні запити до файлів мільйонів рядків, повністю автономні, з даними, які ніколи не залишають вашого комп' ютера.

Повнофункціональний проект доступний за адресою usplucid.CsvLlm - включення CsvQueryService, ConversationalCsvAnalyserІ створення даних, заснованих на Богусі.

Ресурси

DuckDB

Ollama & LLMs

Створення тестових даних

Варіанти

Супутні статті

Finding related posts...
logo

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