🍀

Tabular 'Vibe Modeling' or Its Equivalent with Gemini 3.0 Pro.

に公開

Introduction

Recently, development methods utilizing generative AI are being adopted in various places. Among these, so-called Vibe Coding, where you provide instructions in natural language only, sometimes even building with hands-off driving, is being tested.

In my work, I often have opportunities to train and infer machine learning models using tabular data. Although the data differs for each project, I frequently feel that I am writing similar code every time for preprocessing or pipeline construction, or I think, "Can I offload this customization part to generative AI to make it more efficient? Can I do Vibe Modeling?"

Generally, in generative AI assistance for software development, the quality of the output tends to increase the more specific the instructions given are. In this article, I will generate machine learning pipeline code by varying the instruction level of the prompt (which I will arbitrarily call Vibe Modeling in this article) and examine how the generated code changes and its quality.

Furthermore, this article will not take an approach of iteratively refining the generated code through dialogue; instead, I will experiment with how far I can get with a single initial instruction or through the LLM's autonomous trial and error.


An image of vibe modeling (really?)

Setup

The dataset used in this article is Online Retail II UCI, and the beginning of the data looks like this. I have confirmed the data beforehand before attempting the subsequent code generation.


Input data

For the coding assistance tool, I'm using Gemini CLI, and I have configured the LLM as Gemini 3.0 Pro. Upon checking the execution logs, I also observed instances where the processing was automatically switched to Gemini 2.5 Flash for some operations. I will vary the instruction level of the prompt across the following three stages to confirm how the generated model training and inference code changes, and to check its quality.

    1. Describe only the objective, problem setting, and important precautions.
    01_simple_requirements.txt
    # Requirements for Model Training and Inference Notebook
    - Data to Use
        - ./data.csv
    - Problem Setting
        - Predict purchase amounts for the next 30 days using data from the 30 days before the prediction date
        - Aggregate by CustomerID to create features and target variables
    - Perform data exploration in advance and conduct necessary preprocessing
    - Validation Design
        - Decide the train/validation data split method based on data characteristics
        - Thoroughly check to absolutely avoid leakage
    - Other
        - Write code in `01_simple.py`
        - Run `uv run 01_simple.py` for verification
    
    1. In addition to 1, promote trial and error to encourage improvement.
    02_try_and_error_requirements.txt
    # Requirements for Model Training and Inference Notebook
    - Data to Use
        - ./data.csv
    - Problem Setting
        - Predict purchase amounts for the next 30 days using data from the 30 days before the prediction date
        - Aggregate by CustomerID to create features and target variables
    - Perform data exploration in advance and conduct necessary preprocessing
    - Validation Design
        - Decide the train/validation data split method based on data characteristics
        - Thoroughly check to absolutely avoid leakage
    - Other
        - After design and implementation, carefully consider whether the content is appropriate, verify operation, and improve/fix as needed
        - Write code in `02_try_and_error.py`
        - Run `uv run 02_try_and_error.py` for verification
    
    
    1. In addition to 1, provide detailed descriptions of the pipeline and features (which correspond to what I intend to achieve)
    03_detail_requirements.txt
    # Requirements for Model Training and Inference Notebook
    - Data to Use
        - ./data.csv
    - Problem Setting
        - Based on the prediction date, "predict purchase amounts for the next 30 days using data from the 30 days before the prediction date"
            - This setting may vary depending on data characteristics, business processes, and operational methods, so please consider a better approach based on actual constraints
        - For example, when the prediction date is 2011-03-01
            - Aggregate records from the 90 days before 2011-02-28 to create features
            - Use the sum of purchase amounts for the 30 days from 2011-03-01 onwards as the target variable
        - By shifting the prediction date, multiple sets of features and target variables can be created, which can be combined to prepare training and validation data
    - Based on the results of prior data exploration, perform the following preprocessing
        - Specify `encoding='shift-jis'` when loading data with `pd.read_csv` to avoid encoding errors
        - Type specification for `CustomerID` (`str`)
        - Convert the `InvoiceDate` column to datetime type
        - Remove records with missing `CustomerID`
        - Remove records with negative `Quantity` values
    - First, enable accuracy evaluation with simple features
        - For both features and target variables, aggregation should be unique by CustomerID and prediction_date
        - Features
            - Number of records
            - nunique of InvoiceNo
            - nunique of StockCode
            - nunique of InvoiceDate
            - mode of Country
            - sum, max, min, median of purchase amounts so far
        - Target Variable
            - Sum of (UnitPrice * Quantity)
        - Cross-validation
    
            | Fold | Training Data | Validation Data |
            | --- | --- | --- |
            | 1 | "2011-03-01", "2011-04-01", "2011-05-01" | "2011-06-01" |
            | 2 | "2011-04-01", "2011-05-01", "2011-06-01" | "2011-07-01" |
            | 3 | "2011-05-01", "2011-06-01", "2011-07-01" | "2011-08-01" |
            | 4 | "2011-06-01", "2011-07-01", "2011-08-01" | "2011-09-01" |
            | 5 | "2011-07-01", "2011-08-01", "2011-09-01" | "2011-10-01" |
            | test | "2011-08-01", "2011-09-01", "2011-10-01" | "2011-11-01" |
    
        - Use RMSE, commonly used in regression, as the objective function and evaluation metric
        - Use LightGBM as the model
            - Use the LightGBM Python API, not the scikit-learn wrapper
            - Use model parameters commonly found in public Kaggle notebooks
        - Pipeline
            - Convert categorical features with LabelEncoder before inputting to the model
            - Save artifacts obtained from training (lgb model, label encoder, feature columns) so they can be loaded during inference
            - Structure it so that inference can be executed independently
    - Other
        - After design and implementation, carefully consider whether the content is appropriate, verify operation, and improve on your own as needed
        - Write code in `03_detail.py`
        - Run `uv run 03_detail.py` for verification
    

With the Gemini CLI, I instructed the code implementation while specifying a separately created requirements file. The basic prompt is as follows, and I change the section marked @01_simple_requirements.txt according to each condition.

First, please review @01_simple_requirements.txt.
Based on this document, please implement the model training and inference code.

1. Describe only the objective, problem setting, and important precautions.

I will check what kind of code comes out with a single, one-shot prompt. The requirements only include the objective and the minimum necessary precautions.
The generated code is as follows.

1. Code with only objective and problem setting
# /// script
# requires-python = ">=3.9"
dependencies = [
    "pandas",
    "scikit-learn",
    "numpy",
]
# ///

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score


import warnings

# Suppress warnings (such as FutureWarning)
warnings.filterwarnings('ignore')

def load_and_preprocess(filepath):
    """Load data and perform basic preprocessing"""
    print("Loading data...")
    # Load with date parsing
    try:
        df = pd.read_csv(filepath, parse_dates=['InvoiceDate'], encoding='ISO-8859-1')
    except UnicodeDecodeError:
        df = pd.read_csv(filepath, parse_dates=['InvoiceDate'], encoding='cp1252')
    
    print(f"Original data shape: {df.shape}")

    # Remove data without CustomerID as it cannot be used for per-customer prediction
    df = df.dropna(subset=['CustomerID'])
    df['CustomerID'] = df['CustomerID'].astype(int)

    # Calculate amount
    df['TotalAmount'] = df['Quantity'] * df['UnitPrice']

    # Create date-only column (discard time)
    df['Date'] = df['InvoiceDate'].dt.normalize()
    
    print(f"Data shape after dropping null CustomerID: {df.shape}")
    print(f"Date range: {df['Date'].min()} to {df['Date'].max()}")
    
    return df

def create_features_and_target(df, cutoff_date, target_days=30, feature_days=30):
    """
    Create features and target variable based on the specified cutoff_date

    Features: Data from the period [cutoff_date - feature_days, cutoff_date)
    Target: Data from the period [cutoff_date, cutoff_date + target_days)
    """

    # Define periods
    feature_start = cutoff_date - pd.Timedelta(days=feature_days)
    feature_end = cutoff_date # Up to the day before cutoff_date (not including cutoff_date, or up to 00:00 on cutoff_date)
    
    target_start = cutoff_date
    target_end = cutoff_date + pd.Timedelta(days=target_days)

    # Data filtering
    # Data for features
    feat_df = df[(df['Date'] >= feature_start) & (df['Date'] < feature_end)]

    # Data for target variable
    target_df = df[(df['Date'] >= target_start) & (df['Date'] < target_end)]

    # Base customer list (whether to base on customers who existed in the feature period or all customers)
    # Here, both "customers who had actions in the feature period" and "customers who had actions in the target period" should be considered,
    # but since we're "predicting purchase amounts based on the prediction date...",
    # the actual targets are "customers active at the prediction time" or "all customers".
    # For simplicity, we target customers who have data in either the feature period or the target period.
    # However, during training, the common format is to link "future sales" to "customers for whom features can be obtained".
    # Even if there are no purchases in the feature period, they might be prediction targets if they were registered in the past,
    # but since this dataset has no customer master, we only know customers with transactions.
    # Therefore, we base on customers that exist in feat_df (if no purchases in the last 30 days = 0 yen, we would need to expand the base).

    # Since the requirement is "using data from the last 30 days",
    # we use customers with data in the feature period as the main population,
    # while those with data only in the target period (new/returning) are excluded from the training data this time as they are difficult to predict without historical data,
    # or could be treated as having 0 features.
    # For simplification, we target "customers with purchase history in the feature period".

    base_customers = feat_df['CustomerID'].unique()

    # Feature aggregation
    features = feat_df.groupby('CustomerID').agg({
        'TotalAmount': ['sum', 'mean', 'count'],
        'Quantity': ['sum'],
        'InvoiceDate': ['max'] # For calculating last purchase date
    })
    features.columns = ['_'.join(col).strip() for col in features.columns.values]

    # Recency (cutoff_date - last purchase date)
    features['Recency'] = (cutoff_date - features['InvoiceDate_max'].dt.normalize()).dt.days
    features = features.drop('InvoiceDate_max', axis=1)

    # Rename columns
    features = features.add_prefix('Past30_')

    # Target variable aggregation
    targets = target_df.groupby('CustomerID')['TotalAmount'].sum().rename('Target_Next30_Amount')

    # Join (Left Join: Link correct answers to customers with features. If no correct answer, treat as 0 yen)
    data = pd.DataFrame(index=base_customers)
    data.index.name = 'CustomerID'

    data = data.join(features, how='left')
    data = data.join(targets, how='left')

    # Fill missing values
    # Features shouldn't have missing values since base_customers comes from feat_df, but just in case
    data = data.fillna(0)
    
    return data

def main():
    # 1. Load data
    df = load_and_preprocess('data.csv')

    # Check maximum date in data
    max_date = df['Date'].max()
    min_date = df['Date'].min()
    print(f"Data covers from {min_date} to {max_date}")

    # 2. Validation design
    # Split by time series to avoid leaking future information.
    # Test cutoff date: 30 days before the end of the dataset (this allows using the last 30 days as test data ground truth)
    test_cutoff = max_date - pd.Timedelta(days=30)
    
    print(f"\n--- Preparing Test Set (Cutoff: {test_cutoff}) ---")
    test_data = create_features_and_target(df, test_cutoff)
    print(f"Test data shape: {test_data.shape}")

    # Training cutoff date: Set so it doesn't overlap with the test period.
    # Test period feature period: [test_cutoff - 30, test_cutoff)
    # Test period target variable period: [test_cutoff, test_cutoff + 30)
    #
    # It's safe for training data to use only information before the test data's "feature period",
    # but for training the prediction model itself, we just need "past features" and "past ground truth (future at that point)".
    # To prevent leakage, we need to ensure that the training data's "target variable period" doesn't overlap with the test data's "target variable period" (or doesn't contain information beyond the test data's feature calculation time).
    # The safest approach is for the training data's target variable period end date to be before or on the test data's cutoff date.
    # In other words: Train_Target_End <= Test_Cutoff
    # Train_Cutoff + 30 <= Test_Cutoff
    # Train_Cutoff <= Test_Cutoff - 30

    # Create several training data with 30-day sliding window to increase data
    train_cutoffs = []
    current_cutoff = test_cutoff - pd.Timedelta(days=60) # Start from 60 days before with some margin

    while current_cutoff > min_date + pd.Timedelta(days=30):
        train_cutoffs.append(current_cutoff)
        current_cutoff -= pd.Timedelta(days=30) # Go back 30 days at a time
    
    print(f"\n--- Preparing Train Set (Cutoffs: {[t.date() for t in train_cutoffs]}) ---")
    
    train_dfs = []
    for cutoff in train_cutoffs:
        train_dfs.append(create_features_and_target(df, cutoff))
    
    if not train_dfs:
        print("Error: Not enough data to create training set.")
        return

    train_data = pd.concat(train_dfs)
    print(f"Train data shape: {train_data.shape}")

    # 3. Model training
    X_train = train_data.drop('Target_Next30_Amount', axis=1)
    y_train = train_data['Target_Next30_Amount']

    X_test = test_data.drop('Target_Next30_Amount', axis=1)
    y_test = test_data['Target_Next30_Amount']

    print("\n--- Training Model ---")
    model = RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=-1)
    model.fit(X_train, y_train)

    # 4. Evaluation
    print("\n--- Evaluation ---")
    y_pred = model.predict(X_test)

    rmse = np.sqrt(mean_squared_error(y_test, y_pred))
    mae = mean_absolute_error(y_test, y_pred)
    r2 = r2_score(y_test, y_pred)

    print(f"RMSE: {rmse:.4f}")
    print(f"MAE: {mae:.4f}")
    print(f"R2 Score: {r2:.4f}")

    # Feature importances
    print("\n--- Feature Importances ---")
    importances = pd.DataFrame({
        'feature': X_train.columns,
        'importance': model.feature_importances_
    }).sort_values('importance', ascending=False)
    print(importances)

    # Specific prediction examples
    print("\n--- Prediction Examples (Top 5) ---")
    results = pd.DataFrame({
        'Actual': y_test,
        'Predicted': y_pred
    })
    print(results.head())

if __name__ == "__main__":
    main()

Evaluation of Results

Upon examining the code contents, several positive aspects were found. First, in the initial generation, it confirmed that an encoding error occurred when executing pd.read_csv and modified it to specify the appropriate encoding. Additionally, the training and test data were split by offsetting their time periods, showing awareness of leakage prevention. Furthermore, it incorporated the perspective of RFM analysis (Recency, Frequency, Monetary), which is common in marketing analysis, and created features considering Recency and Monetary.
On the other hand, there were several areas for improvement. First, despite having sufficient data, cross-validation was not included to improve generalization performance. Also, Random Forest was used as the model, whereas gradient boosting models such as LightGBM, which are recommended by the Kaggle community as a first choice for tabular data, would have been preferable.
In data preprocessing, prior data exploration revealed that Quantity contained negative values, and since there was no apparent reason or explanation for them, it would be prudent to remove records with negative values. However, the generated code did not consider negative values and aggregated them as-is. There were also no features related to Frequency. The lack of inference-only processing was another area that fell short.

2. In addition to 1, promote trial and error to encourage improvement.

Next is a condition where I instruct the LLM to iterate and improve through trial and error in addition to 1. I expect the LLM to analyze the data itself and write better code. Gemini first generated and executed code to analyze the data and verify it.

2. Code when conveying design overview and encouraging improvement through trial and error (Data Exploration)
# /// script
# requires-python = ">=3.10"
dependencies = [
    "pandas",
    "numpy",
]
# ///

import pandas as pd
import numpy as np

def analyze_data():
    print("Loading data...")
    # Load with date parsing specified
    try:
        df = pd.read_csv('./data.csv', parse_dates=['InvoiceDate'])
    except Exception as e:
        print(f"Failed to read csv with date parsing: {e}")
        # The format might be special, so read normally
        df = pd.read_csv('./data.csv')
        df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'])

    print("\nData Info:")
    print(df.info())

    print("\nMissing Values:")
    print(df.isnull().sum())

    print("\nData Range:")
    print(f"Start: {df['InvoiceDate'].min()}")
    print(f"End: {df['InvoiceDate'].max()}")

    # Check data without CustomerID since per-customer prediction is not possible
    missing_customer = df['CustomerID'].isnull().sum()
    print(f"\nMissing CustomerID count: {missing_customer} ({missing_customer/len(df):.2%})")

    # Descriptive statistics
    print("\nDescriptive Stats:")
    print(df.describe())

if __name__ == "__main__":
    analyze_data()

Based on these analysis results, the generated training code is as follows.

2. Code when conveying design overview and encouraging improvement through trial and error (Training and Inference)
# /// script
# requires-python = ">=3.10"
dependencies = [
    "pandas",
    "numpy",
    "scikit-learn",
    "lightgbm",
]
# ///

import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.metrics import mean_squared_error, mean_absolute_error
import warnings

# Suppress warnings for cleaner output
warnings.filterwarnings('ignore')

def load_data(file_path='./data.csv'):
    """
    Load data from CSV file. Handles different encodings.
    """
    print(f"Loading data from {file_path}...")
    try:
        # Try default UTF-8 first
        df = pd.read_csv(file_path, parse_dates=['InvoiceDate'])
    except UnicodeDecodeError:
        # Fallback to ISO-8859-1 (common for UK data)
        print("UTF-8 decode failed, trying ISO-8859-1...")
        df = pd.read_csv(file_path, encoding='ISO-8859-1', parse_dates=['InvoiceDate'])
    except Exception as e:
        print(f"Error loading data: {e}")
        return None
    return df

def preprocess(df):
    """
    Basic preprocessing:
    - Remove missing CustomerID
    - Calculate TotalAmount
    - Ensure correct data types
    """
    print("Preprocessing data...")
    
    # Drop missing CustomerID as we need to aggregate by customer
    initial_rows = len(df)
    df = df.dropna(subset=['CustomerID'])
    print(f"Dropped {initial_rows - len(df)} rows with missing CustomerID.")
    
    df['CustomerID'] = df['CustomerID'].astype(int)
    
    # Calculate TotalAmount
    df['TotalAmount'] = df['Quantity'] * df['UnitPrice']
    
    # Ensure InvoiceDate is datetime
    df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'])
    
    return df

def create_features(df_history, ref_date):
    """
    Calculate features from historical data for each customer.
    """
    # Aggregations
    aggs = df_history.groupby('CustomerID').agg({
        'TotalAmount': ['sum', 'mean', 'count', 'std', 'min', 'max'],
        'Quantity': ['sum', 'mean'],
        'InvoiceDate': ['max'] # Used for Recency
    })
    
    # Flatten MultiIndex columns
    aggs.columns = ['_'.join(col).strip() for col in aggs.columns.values]
    
    # Recency: Days since last purchase
    # Note: (ref_date - last_purchase)
    aggs['Recency'] = (ref_date - aggs['InvoiceDate_max']).dt.days
    aggs = aggs.drop(columns=['InvoiceDate_max'])
    
    # Fill NaN values that might result from std calculation with single record
    aggs = aggs.fillna(0)
    
    return aggs

def create_dataset(df, lookback_days=30, target_days=30, step_days=7):
    """
    Create a dataset using a sliding window approach.
    
    Parameters:
    - lookback_days: Number of days to look back for features (X)
    - target_days: Number of days to look forward for target (y)
    - step_days: Step size for moving the window
    """
    start_date = df['InvoiceDate'].min()
    end_date = df['InvoiceDate'].max()
    
    print(f"Data Date Range: {start_date} to {end_date}")
    
    # Generate reference dates
    # Logic:
    # Feature Window: [ref_date - lookback, ref_date - 1]
    # Target Window:  [ref_date, ref_date + target - 1]
    
    # First ref_date must allow for full lookback
    first_ref = start_date + pd.Timedelta(days=lookback_days)
    # Last ref_date must allow for full target window
    last_ref = end_date - pd.Timedelta(days=target_days)
    
    if first_ref > last_ref:
        print("Error: Data duration is too short for the specified lookback and target periods.")
        return None, None, None

    ref_dates = pd.date_range(start=first_ref, end=last_ref, freq=f'{step_days}D')
    
    print(f"Generating dataset for {len(ref_dates)} reference dates (Sliding Window)...")
    
    X_list = []
    y_list = []
    meta_list = [] 
    
    # Sort dataframe once for faster slicing
    df = df.sort_values('InvoiceDate')
    
    for ref_date in ref_dates:
        # Define time windows
        feat_start = ref_date - pd.Timedelta(days=lookback_days)
        feat_end = ref_date - pd.Timedelta(days=1)
        
        target_start = ref_date
        target_end = ref_date + pd.Timedelta(days=target_days) # Exclusive in slice logic below if we use <
        
        # Slicing
        # Note: slice is inclusive for start, inclusive for end if using label-based indexing with datetime index,
        # but here we use boolean masking.
        # Features: [start, end] inclusive
        mask_feat = (df['InvoiceDate'] >= feat_start) & (df['InvoiceDate'] <= feat_end)
        df_feat = df.loc[mask_feat]
        
        # Target: [start, end) - let's be precise: target_days from ref_date
        mask_target = (df['InvoiceDate'] >= target_start) & (df['InvoiceDate'] < target_start + pd.Timedelta(days=target_days))
        df_target = df.loc[mask_target]
        
        if df_feat.empty:
            continue
            
        # 1. Calculate Features
        # We focus on customers who were active in the lookback period.
        features = create_features(df_feat, ref_date)
        
        # 2. Calculate Target
        # Sum of TotalAmount per customer in target window
        targets = df_target.groupby('CustomerID')['TotalAmount'].sum().rename('TargetAmount')
        
        # 3. Merge
        # Left join: We predict for customers observed in the feature window.
        # Customers who don't buy in the target window get 0 target amount.
        dataset = features.join(targets, how='left').fillna({'TargetAmount': 0})
        
        # Store metadata
        dataset['RefDate'] = ref_date
        
        X_list.append(dataset.drop(columns=['TargetAmount', 'RefDate']))
        y_list.append(dataset['TargetAmount'])
        meta_list.append(dataset[['RefDate']].reset_index()) # Includes CustomerID

    if not X_list:
        return None, None, None

    X = pd.concat(X_list)
    y = pd.concat(y_list)
    meta = pd.concat(meta_list)
    
    return X, y, meta

def train_and_evaluate(X, y, meta):
    """
    Split data based on time, train model, and evaluate.
    """
    # Chronological Split
    # Use the last 20% of reference dates for testing to avoid leakage
    unique_dates = sorted(meta['RefDate'].unique())
    split_idx = int(len(unique_dates) * 0.8)
    split_date = unique_dates[split_idx]
    
    print(f"\nSplitting data at RefDate: {split_date}")
    
    # Create masks based on RefDate aligned with X/y rows
    train_mask = meta['RefDate'] < split_date
    test_mask = meta['RefDate'] >= split_date
    
    X_train = X[train_mask.values]
    y_train = y[train_mask.values]
    X_test = X[test_mask.values]
    y_test = y[test_mask.values]
    
    print(f"Train set: {X_train.shape[0]} samples")
    print(f"Test set:  {X_test.shape[0]} samples")
    
    # Model Training (LightGBM)
    print("\nTraining LightGBM model...")
    model = lgb.LGBMRegressor(
        n_estimators=500,
        learning_rate=0.05,
        num_leaves=31,
        random_state=42,
        n_jobs=-1
    )
    model.fit(X_train, y_train)
    
    # Prediction
    preds = model.predict(X_test)
    
    # Metrics
    rmse = np.sqrt(mean_squared_error(y_test, preds))
    mae = mean_absolute_error(y_test, preds)
    
    print("------------------------------")
    print(f"RMSE: {rmse:.4f}")
    print(f"MAE:  {mae:.4f}")
    print("------------------------------")
    
    # Feature Importance
    importance = pd.DataFrame({
        'feature': X_train.columns,
        'importance': model.feature_importances_
    }).sort_values('importance', ascending=False)
    
    print("\nTop 5 Features:")
    print(importance.head(5))
    
    return model, preds, y_test

def main():
    # 1. Load
    df = load_data()
    if df is None:
        return

    # 2. Preprocess
    df = preprocess(df)
    
    # 3. Create Dataset
    print("\n--- Creating Dataset ---")
    X, y, meta = create_dataset(df, lookback_days=30, target_days=30, step_days=7)
    
    if X is None:
        print("Failed to create dataset.")
        return
        
    print(f"Total samples created: {X.shape[0]}")
    
    # 4. Train & Evaluate
    print("\n--- Model Training & Evaluation ---")
    train_and_evaluate(X, y, meta)

if __name__ == "__main__":
    main()

Evaluation of Results

Although it didn't iterate as much as I had expected, the following improvements were made in the initial output, resulting in the final code:

  • Added comments
  • Added LightGBM parameters
  • Properly functionalized and refactored the processing within the main function

However, the main points regarding positive aspects and areas for improvement were not much different from 1. Even when including instructions to "iterate and improve," it seems difficult to achieve clear improvements unless specific implementation goals and requirements such as features to prioritize are defined.

3. In addition to 1, provide detailed descriptions of the pipeline and features

Finally, this is a pattern where humans check the data in advance and write specific instructions based on that content. In the requirements document for 3, I described the pipeline structure and features to be created as much as I could think of.

The generated code is as follows.

3. Describing detailed pipeline and features
# /// script
# requires-python = ">=3.9"
dependencies = [
    "pandas",
    "numpy",
    "lightgbm",
    "scikit-learn",
]
# ///

import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import LabelEncoder
import pickle
import os
import warnings

warnings.filterwarnings('ignore')

# Configuration
INPUT_FILE = './data.csv'
ARTIFACTS_DIR = './artifacts'
os.makedirs(ARTIFACTS_DIR, exist_ok=True)

# Feature generation settings
# The requirements document says "30 days of data up to the previous day", but the example says "aggregate 90 days of records",
# so we adopt the 90 days from the example considering the amount of information.
FEATURE_WINDOW_DAYS = 90
TARGET_WINDOW_DAYS = 30

def load_and_preprocess(file_path):
    print("Loading data...")
    # Specify encoding='shift-jis' when loading data with pd.read_csv
    # CustomerID cannot be specified as str at load time in some cases (becomes float when NA exists), so convert later
    df = pd.read_csv(file_path, encoding='shift-jis')

    # Convert InvoiceDate column to datetime type
    df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'])

    # Remove records with missing CustomerID
    df = df.dropna(subset=['CustomerID'])

    # Convert CustomerID to str (e.g., 12345.0 -> "12345")
    df['CustomerID'] = df['CustomerID'].astype(float).astype(int).astype(str)

    # Remove records with negative Quantity values
    df = df[df['Quantity'] > 0]

    # Add amount column
    df['Amount'] = df['Quantity'] * df['UnitPrice']
    
    return df.reset_index(drop=True)

def create_features(df, prediction_date):
    """
    Create features based on the specified prediction_date
    Target data: [prediction_date - FEATURE_WINDOW_DAYS, prediction_date)
    """
    start_date = prediction_date - pd.Timedelta(days=FEATURE_WINDOW_DAYS)
    end_date = prediction_date

    mask = (df['InvoiceDate'] >= start_date) & (df['InvoiceDate'] < end_date)
    target_df = df[mask]

    if target_df.empty:
        return pd.DataFrame()

    # Function to get mode of Country
    def get_mode(x):
        m = x.mode()
        if not m.empty:
            return m.iloc[0]
        return np.nan

    # Aggregation definitions
    # InvoiceNo: nunique
    # StockCode: nunique
    # InvoiceDate: nunique
    # Country: mode
    # Amount: sum, max, min, median
    # count: Use count of InvoiceNo as the number of records
    agg_funcs = {
        'InvoiceNo': ['count', 'nunique'],
        'StockCode': ['nunique'],
        'InvoiceDate': ['nunique'],
        'Country': [get_mode],
        'Amount': ['sum', 'max', 'min', 'median']
    }

    grouped = target_df.groupby('CustomerID').agg(agg_funcs)

    # Flatten column names
    new_cols = []
    for col, stat in grouped.columns:
        if stat == '<lambda>' or stat == 'get_mode': # get_mode
            stat = 'mode'
        if col == 'InvoiceNo' and stat == 'count':
            new_cols.append('record_count')
        else:
            new_cols.append(f'{col}_{stat}')

    grouped.columns = new_cols
    grouped = grouped.reset_index()

    # Add prediction_date
    grouped['prediction_date'] = prediction_date

    return grouped

def create_target(df, prediction_date):
    """
    Create target variable based on the specified prediction_date
    Target data: [prediction_date, prediction_date + TARGET_WINDOW_DAYS)
    """
    start_date = prediction_date
    end_date = prediction_date + pd.Timedelta(days=TARGET_WINDOW_DAYS)
    
    mask = (df['InvoiceDate'] >= start_date) & (df['InvoiceDate'] < end_date)
    target_df = df[mask]
    
    if target_df.empty:
        return pd.DataFrame(columns=['CustomerID', 'target_amount'])

    grouped = target_df.groupby('CustomerID')['Amount'].sum().reset_index()
    grouped.rename(columns={'Amount': 'target_amount'}, inplace=True)
    
    return grouped

def make_dataset(df, prediction_dates, is_train=True):
    features_list = []
    
    for date_str in prediction_dates:
        pred_date = pd.to_datetime(date_str)
        # print(f"Processing prediction date: {pred_date}")

        # Create features
        feats = create_features(df, pred_date)
        if feats.empty:
            continue

        if is_train:
            # Create target variable
            targets = create_target(df, pred_date)
            # Left join: Join target to users with features
            merged = pd.merge(feats, targets, on='CustomerID', how='left')
            # Set users with no purchases in the period to 0 yen
            merged['target_amount'] = merged['target_amount'].fillna(0)
            features_list.append(merged)
        else:
            features_list.append(feats)
            
    if not features_list:
        return pd.DataFrame()
        
    return pd.concat(features_list, ignore_index=True)

def main():
    # 1. Load data
    df = load_and_preprocess(INPUT_FILE)
    print(f"Data loaded. Shape: {df.shape}")

    # 2. Define folds
    folds = [
        {
            'train': ["2011-03-01", "2011-04-01", "2011-05-01"],
            'valid': ["2011-06-01"]
        },
        {
            'train': ["2011-04-01", "2011-05-01", "2011-06-01"],
            'valid': ["2011-07-01"]
        },
        {
            'train': ["2011-05-01", "2011-06-01", "2011-07-01"],
            'valid': ["2011-08-01"]
        },
        {
            'train': ["2011-06-01", "2011-07-01", "2011-08-01"],
            'valid': ["2011-09-01"]
        },
        {
            'train': ["2011-07-01", "2011-08-01", "2011-09-01"],
            'valid': ["2011-10-01"]
        }
    ]
    
    # Test set definition
    test_set_def = {
        'train': ["2011-08-01", "2011-09-01", "2011-10-01"],
        'test': ["2011-11-01"]
    }

    # Prepare label encoding
    print("Preparing label encoder...")
    le = LabelEncoder()
    # Assuming no missing values, but use astype(str) just in case
    le.fit(df['Country'].astype(str).unique())

    # Save
    with open(os.path.join(ARTIFACTS_DIR, 'label_encoder.pkl'), 'wb') as f:
        pickle.dump(le, f)

    # Model parameters
    params = {
        'objective': 'regression',
        'metric': 'rmse',
        'boosting_type': 'gbdt',
        'learning_rate': 0.05,
        'num_leaves': 31,
        'feature_fraction': 0.9,
        'bagging_fraction': 0.8,
        'bagging_freq': 5,
        'verbose': -1,
        'seed': 42
    }

    # Cross-validation loop
    models = []
    rmse_scores = []
    feature_columns = None

    print("Starting cross-validation...")
    for i, fold in enumerate(folds):
        print(f"\n=== Fold {i+1} ===")

        # Create training data
        train_df = make_dataset(df, fold['train'], is_train=True)
        valid_df = make_dataset(df, fold['valid'], is_train=True)

        if train_df.empty or valid_df.empty:
            print("Skipping fold due to empty data.")
            continue

        # Identify feature columns
        drop_cols = ['CustomerID', 'prediction_date', 'target_amount']
        features = [c for c in train_df.columns if c not in drop_cols]
        feature_columns = features # Keep for saving

        X_train = train_df[features].copy()
        y_train = train_df['target_amount']
        X_valid = valid_df[features].copy()
        y_valid = valid_df['target_amount']

        # Convert categorical variables
        if 'Country_mode' in X_train.columns:
            X_train['Country_mode'] = le.transform(X_train['Country_mode'].astype(str))
            X_valid['Country_mode'] = le.transform(X_valid['Country_mode'].astype(str))
            categorical_feats = ['Country_mode']
        else:
            categorical_feats = []

        # LGBM Dataset
        lgb_train = lgb.Dataset(X_train, y_train, categorical_feature=categorical_feats)
        lgb_eval = lgb.Dataset(X_valid, y_valid, reference=lgb_train, categorical_feature=categorical_feats)

        # Training
        model = lgb.train(
            params,
            lgb_train,
            num_boost_round=1000,
            valid_sets=[lgb_train, lgb_eval],
            callbacks=[
                lgb.early_stopping(stopping_rounds=50),
                lgb.log_evaluation(period=50)
            ]
        )
        
        models.append(model)

        # Evaluation
        y_pred = model.predict(X_valid, num_iteration=model.best_iteration)
        rmse = np.sqrt(mean_squared_error(y_valid, y_pred))
        rmse_scores.append(rmse)
        print(f"Fold {i+1} RMSE: {rmse:.4f}")

    if rmse_scores:
        print(f"\nAverage RMSE: {np.mean(rmse_scores):.4f}")

    # Save artifacts
    if models:
        print("\nSaving artifacts...")
        best_model = models[-1] # Use the last model for simplicity
        best_model.save_model(os.path.join(ARTIFACTS_DIR, 'lgb_model.txt'))

        with open(os.path.join(ARTIFACTS_DIR, 'feature_columns.pkl'), 'wb') as f:
            pickle.dump(feature_columns, f)
        print(f"Artifacts saved to {ARTIFACTS_DIR}")

    # Inference test
    print("\n=== Inference on Test Set (Simulation) ===")
    # Perform prediction on test set dates
    test_date_str = test_set_def['test'][0]
    print(f"Predicting for date: {test_date_str}")

    test_features_df = make_dataset(df, [test_date_str], is_train=False)

    if not test_features_df.empty and os.path.exists(os.path.join(ARTIFACTS_DIR, 'feature_columns.pkl')):
        # Load
        with open(os.path.join(ARTIFACTS_DIR, 'feature_columns.pkl'), 'rb') as f:
            loaded_features = pickle.load(f)
        with open(os.path.join(ARTIFACTS_DIR, 'label_encoder.pkl'), 'rb') as f:
            loaded_le = pickle.load(f)
        loaded_model = lgb.Booster(model_file=os.path.join(ARTIFACTS_DIR, 'lgb_model.txt'))

        X_test = test_features_df[loaded_features].copy()

        if 'Country_mode' in X_test.columns:
            X_test['Country_mode'] = loaded_le.transform(X_test['Country_mode'].astype(str))

        preds = loaded_model.predict(X_test)
        test_features_df['predicted_amount'] = preds

        # Display
        print("Top 5 predictions:")
        print(test_features_df[['CustomerID', 'prediction_date', 'predicted_amount']].head())

        # Evaluation (compare with actual values)
        print("Evaluating test set performance...")
        actual_target = create_target(df, pd.to_datetime(test_date_str))
        merged_res = pd.merge(test_features_df, actual_target, on='CustomerID', how='left')
        merged_res['target_amount'] = merged_res['target_amount'].fillna(0)

        test_rmse = np.sqrt(mean_squared_error(merged_res['target_amount'], merged_res['predicted_amount']))
        print(f"Test Set RMSE: {test_rmse:.4f}")

if __name__ == "__main__":
    main()

Evaluation of Results

Although the content described in the requirements document is mostly reflected in the implementation, there are still some areas for improvement upon closer examination. For example, the LabelEncoder fit is performed before splitting the training and validation data, which poses a risk of leakage. Additionally, the configuration doesn't output training progress logs, and personally I would appreciate being able to check progress during training (though I think this is a matter of personal preference).
However, with this code, it seems possible to move on to the next steps such as trial and error for accuracy improvement with just minor adjustments. In the cases of 1 and 2, there were many additional adjustments needed, resulting in the need to continue conversations or add implementations myself. Personally (partly because I don't particularly enjoy frequent back-and-forth conversations), method 3 seemed to best suit my preferences. I think the balance in this regard is also influenced by individual preferences.

Conclusion

In this experiment, Vibe Modeling (my arbitrary term) for machine learning model training and inference code using generative AI did not produce significantly off-target code even with minimal instructions, but it did leave some areas where I thought "I wish it did this differently." When describing what I wanted to achieve in detail, it was mostly reflected in the implementation, but the human side needed to patiently perform data exploration before being able to describe what they wanted, and naturally, I felt a trade-off between the effort of preparation and the quality of the generated output.
While the effort of implementation itself can be saved and code generation can be leveraged even for data exploration, making it much easier than implementing from scratch, my honest current impression is that I'm still uncertain about the balance of work division between AI and humans.
Model performance will continue to improve in the future, and I hope to continue exploring comfortable workflows and division strategies.

GitHubで編集を提案

Discussion