DEV Community

Sajjad Rahman
Sajjad Rahman

Posted on

ML_Log_Target_PCF

# 03_ML_Log_Target_PCF.ipynb

This experiment tests log-transformed PCF as the target.

The important difference from Notebook 02 is:

Component Experiment 01 Experiment 02 Experiment 03
PCF target Raw Raw log1p(PCF)
Product Weight Raw Log Raw
Train/test split Same Same Same
Models 7 7 7
Tuning 3 3 3
Evaluation MAE/RMSE/R² MAE/RMSE/R² MAE/RMSE/R² on original PCF scale

There is one crucial technical difference: the model will train on log1p(PCF), but we must convert predictions back to the original PCF scale before calculating MAE, RMSE and R².

Otherwise, Experiment 03 would not be directly comparable with Experiments 01 and 02.


Notebook 03 — 03_ML_Log_Target_PCF.ipynb

Cell 1 — Markdown

# BEACON Machine Learning — Log Target Experiment

## Experiment 03: Log-Transformed PCF Target

### Purpose

This experiment investigates whether logarithmic transformation of the
PCF target improves machine-learning prediction performance.

The PCF target is highly right-skewed. Therefore, `log1p(PCF)` is used
during model training to reduce the influence of extreme target values.

Product Weight remains in its original representation.

The experiment follows the same dataset, train-test split,
preprocessing strategy, regression models and evaluation procedure used
in Experiments 01 and 02.

### Experimental comparison

Experiment 01:
- Raw PCF target
- Raw Product Weight

Experiment 02:
- Raw PCF target
- Log-transformed Product Weight

Experiment 03:
- Log-transformed PCF target
- Raw Product Weight

### Evaluation

Models are trained using the logarithmic PCF target. Predictions are
converted back to the original PCF scale using `expm1()` before
calculating MAE, RMSE and R².

This allows the results to remain directly comparable with Experiments
01 and 02.
Enter fullscreen mode Exit fullscreen mode

Cell 2 — Imports

# ============================================================
# 1. IMPORT LIBRARIES
# ============================================================

import numpy as np
import pandas as pd

from sklearn.model_selection import (
    train_test_split,
    GridSearchCV
)

from sklearn.preprocessing import OneHotEncoder

from sklearn.linear_model import (
    LinearRegression,
    ElasticNet,
    BayesianRidge
)

from sklearn.ensemble import (
    RandomForestRegressor,
    ExtraTreesRegressor,
    HistGradientBoostingRegressor
)

from sklearn.metrics import (
    mean_absolute_error,
    mean_squared_error,
    r2_score
)

from xgboost import XGBRegressor

from category_encoders import TargetEncoder

import warnings

warnings.filterwarnings("ignore")

RANDOM_STATE = 42
Enter fullscreen mode Exit fullscreen mode

Cell 3 — Load dataset

# ============================================================
# 2. LOAD ORIGINAL CARBON CATALOGUE DATA
# ============================================================

DATA_PATH = (
    "/kaggle/input/datasets/"
    "sajjadrahman56/product-level-of-pcf/"
    "ProductLevel.csv"
)

df = pd.read_csv(
    DATA_PATH,
    encoding="latin1"
)

print("Original dataset shape:", df.shape)
Enter fullscreen mode Exit fullscreen mode

Cell 4 — Create ML dataset

# ============================================================
# 3. CREATE ML DATASET
# ============================================================

df_ml = pd.DataFrame()

df_ml["Year"] = df["Year of reporting"]

df_ml["Company"] = df["Company"]

df_ml["Stage_Level_CO2e_Available"] = (
    df["*Stage-level CO2e available"]
)

df_ml["Country"] = (
    df["Country (where company is incorporated)"]
)

df_ml["Industry"] = (
    df["Company's GICS Industry"]
)

df_ml["Product_Weight"] = (
    df["Product weight (kg)"]
)

df_ml["PCF_Protocol"] = (
    df["Protocol used for PCF"]
)

df_ml["PCF"] = (
    df["Product's carbon footprint (PCF, kg CO2e)"]
)

print("ML dataset shape:", df_ml.shape)

display(df_ml.head())
Enter fullscreen mode Exit fullscreen mode

Cell 5 — Clean PCF

# ============================================================
# 4. CLEAN PCF TARGET
# ============================================================

df_ml["PCF"] = pd.to_numeric(
    df_ml["PCF"],
    errors="coerce"
)

df_ml = df_ml[
    df_ml["PCF"].notna()
].copy()

df_ml = df_ml[
    df_ml["PCF"] >= 0
].copy()

print("Usable rows:", len(df_ml))

print("\nOriginal PCF summary:")
display(
    df_ml["PCF"].describe()
)

print(
    "\nOriginal PCF skewness:",
    df_ml["PCF"].skew()
)
Enter fullscreen mode Exit fullscreen mode

You should again have:

866
Enter fullscreen mode Exit fullscreen mode

Cell 6 — Define X and raw y

# ============================================================
# 5. DEFINE FEATURES AND RAW TARGET
# ============================================================

FEATURES = [
    "Year",
    "Company",
    "Stage_Level_CO2e_Available",
    "Country",
    "Industry",
    "Product_Weight",
    "PCF_Protocol"
]

TARGET = "PCF"

X = df_ml[
    FEATURES
].copy()

y = df_ml[
    TARGET
].copy()

print("X shape:", X.shape)
print("y shape:", y.shape)
Enter fullscreen mode Exit fullscreen mode

Cell 7 — Train/test split

Exactly the same as the first two notebooks.

# ============================================================
# 6. TRAIN / TEST SPLIT
# ============================================================

X_train, X_test, y_train_raw, y_test_raw = train_test_split(
    X,
    y,
    test_size=0.20,
    random_state=RANDOM_STATE
)

print("Training rows:", len(X_train))
print("Testing rows :", len(X_test))
Enter fullscreen mode Exit fullscreen mode

Expected:

Training rows: 692
Testing rows : 174
Enter fullscreen mode Exit fullscreen mode

Cell 8 — Create safe copies

# ============================================================
# 7. CREATE SAFE COPIES
# ============================================================

X_train = X_train.copy()
X_test = X_test.copy()

y_train_raw = y_train_raw.copy()
y_test_raw = y_test_raw.copy()
Enter fullscreen mode Exit fullscreen mode

Cell 9 — Create log target

This is the major change in Notebook 03.

# ============================================================
# 8. LOG-TRANSFORM PCF TARGET
#
# log1p(x) = log(1 + x)
# ============================================================

y_train = np.log1p(
    y_train_raw
)

y_test = np.log1p(
    y_test_raw
)

print("Raw PCF skewness:")
print(
    y_train_raw.skew()
)

print(
    "\nLog-transformed PCF skewness:"
)

print(
    y_train.skew()
)
Enter fullscreen mode Exit fullscreen mode

You should see something similar to:

Raw PCF skewness: ~15+
Log-transformed PCF skewness: much lower
Enter fullscreen mode Exit fullscreen mode

Cell 10 — Keep original y for evaluation

This is very important.

# ============================================================
# 9. STORE ORIGINAL TARGET FOR FINAL EVALUATION
# ============================================================

y_test_original = y_test_raw.copy()

print(
    "Training target used by models: log1p(PCF)"
)

print(
    "Evaluation target: original PCF"
)
Enter fullscreen mode Exit fullscreen mode

Cell 11 — Country → Region

Same as the previous notebooks.

# ============================================================
# 10. COUNTRY → REGION
# ============================================================

country_to_region = {

    # North America
    "USA": "North America",
    "Canada": "North America",

    # Europe
    "Germany": "Europe",
    "Netherlands": "Europe",
    "United Kingdom": "Europe",
    "Switzerland": "Europe",
    "Sweden": "Europe",
    "Finland": "Europe",
    "Italy": "Europe",
    "France": "Europe",
    "Spain": "Europe",
    "Belgium": "Europe",
    "Ireland": "Europe",
    "Luxembourg": "Europe",
    "Lithuania": "Europe",
    "Greece": "Europe",

    # East Asia
    "Japan": "East Asia",
    "Taiwan": "East Asia",
    "South Korea": "East Asia",
    "China": "East Asia",

    # South Asia
    "India": "South Asia",

    # Southeast Asia
    "Malaysia": "Southeast Asia",
    "Indonesia": "Southeast Asia",

    # South America
    "Brazil": "South America",
    "Chile": "South America",
    "Colombia": "South America",

    # Africa
    "South Africa": "Africa",

    # Oceania
    "Australia": "Oceania"
}

X_train["Region"] = X_train[
    "Country"
].map(country_to_region)

X_test["Region"] = X_test[
    "Country"
].map(country_to_region)

X_train["Region"] = (
    X_train["Region"].fillna("Other")
)

X_test["Region"] = (
    X_test["Region"].fillna("Other")
)

X_train.drop(
    columns="Country",
    inplace=True
)

X_test.drop(
    columns="Country",
    inplace=True
)
Enter fullscreen mode Exit fullscreen mode

Cell 12 — Rare Industry

# ============================================================
# 11. RARE INDUSTRY → OTHER
# ============================================================

industry_counts = X_train[
    "Industry"
].value_counts()

rare_industries = industry_counts[
    industry_counts < 10
].index

X_train["Industry"] = X_train[
    "Industry"
].replace(
    rare_industries,
    "Other"
)

X_test["Industry"] = X_test[
    "Industry"
].replace(
    rare_industries,
    "Other"
)
Enter fullscreen mode Exit fullscreen mode

Cell 13 — PCF Protocol

# ============================================================
# 12. PCF PROTOCOL → TOP 5 + OTHER
# ============================================================

top_protocols = [
    "ISO",
    "Not reported",
    "GHGP",
    "PAS2050",
    "TRACI 2.1"
]

X_train["PCF_Protocol"] = X_train[
    "PCF_Protocol"
].where(
    X_train["PCF_Protocol"].isin(
        top_protocols
    ),
    "Other"
)

X_test["PCF_Protocol"] = X_test[
    "PCF_Protocol"
].where(
    X_test["PCF_Protocol"].isin(
        top_protocols
    ),
    "Other"
)
Enter fullscreen mode Exit fullscreen mode

Cell 14 — Stage-level CO2e

# ============================================================
# 13. STAGE-LEVEL CO2e → BINARY
# ============================================================

binary_map = {
    "No": 0,
    "Yes": 1
}

X_train[
    "Stage_Level_CO2e_Available"
] = (
    X_train[
        "Stage_Level_CO2e_Available"
    ]
    .map(binary_map)
    .fillna(0)
)

X_test[
    "Stage_Level_CO2e_Available"
] = (
    X_test[
        "Stage_Level_CO2e_Available"
    ]
    .map(binary_map)
    .fillna(0)
)
Enter fullscreen mode Exit fullscreen mode

Cell 15 — Product Weight winsorisation

Here we return to the raw Product Weight representation.

# ============================================================
# 14. PRODUCT WEIGHT — WINSORISATION
#
# No logarithmic transformation is applied to Product Weight
# in Experiment 03.
# ============================================================

Q1 = X_train[
    "Product_Weight"
].quantile(0.25)

Q3 = X_train[
    "Product_Weight"
].quantile(0.75)

IQR = Q3 - Q1

lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR

print("Winsorisation bounds")
print("Lower:", lower)
print("Upper:", upper)

X_train[
    "Product_Weight"
] = X_train[
    "Product_Weight"
].clip(
    lower=lower,
    upper=upper
)

X_test[
    "Product_Weight"
] = X_test[
    "Product_Weight"
].clip(
    lower=lower,
    upper=upper
)
Enter fullscreen mode Exit fullscreen mode

Cell 16 — Company target encoding

# ============================================================
# 15. COMPANY → TARGET ENCODING
#
# IMPORTANT:
# The encoder is fitted using y_train, which is the log PCF
# target in this experiment.
# ============================================================

te = TargetEncoder(
    cols=["Company"],
    min_samples_leaf=20,
    smoothing=10
)

X_train = te.fit_transform(
    X_train,
    y_train
)

X_test = te.transform(
    X_test
)
Enter fullscreen mode Exit fullscreen mode

Cell 17 — One-hot encoding

# ============================================================
# 16. ONE-HOT ENCODING
# ============================================================

categorical_features = [
    "Industry",
    "PCF_Protocol",
    "Region"
]

encoder = OneHotEncoder(
    drop="first",
    handle_unknown="ignore",
    sparse_output=False
)

encoded_train = encoder.fit_transform(
    X_train[
        categorical_features
    ]
)

encoded_test = encoder.transform(
    X_test[
        categorical_features
    ]
)

encoded_train_df = pd.DataFrame(
    encoded_train,
    columns=encoder.get_feature_names_out(
        categorical_features
    ),
    index=X_train.index
)

encoded_test_df = pd.DataFrame(
    encoded_test,
    columns=encoder.get_feature_names_out(
        categorical_features
    ),
    index=X_test.index
)

X_train = pd.concat(
    [
        X_train.drop(
            columns=categorical_features
        ),
        encoded_train_df
    ],
    axis=1
)

X_test = pd.concat(
    [
        X_test.drop(
            columns=categorical_features
        ),
        encoded_test_df
    ],
    axis=1
)
Enter fullscreen mode Exit fullscreen mode

Cell 18 — Final feature check

# ============================================================
# 17. FINAL FEATURE CHECK
# ============================================================

print(
    "Final X_train shape:",
    X_train.shape
)

print(
    "Final X_test shape:",
    X_test.shape
)

print(
    "\nProduct Weight present:",
    "Product_Weight" in X_train.columns
)

print(
    "Log Product Weight present:",
    "Log_Product_Weight" in X_train.columns
)

print(
    "Target transformation: log1p(PCF)"
)

print(
    "\nAll features numerical:",
    X_train.select_dtypes(
        exclude="number"
    ).empty
)
Enter fullscreen mode Exit fullscreen mode

Expected:

Product Weight present: True
Log Product Weight present: False
Target transformation: log1p(PCF)
All features numerical: True
Enter fullscreen mode Exit fullscreen mode

Cell 19 — Seven baseline models

# ============================================================
# 18. DEFINE BASELINE ML MODELS
# ============================================================

models = {

    "Linear Regression":
        LinearRegression(),

    "ElasticNet":
        ElasticNet(
            random_state=RANDOM_STATE
        ),

    "Bayesian Ridge":
        BayesianRidge(),

    "Random Forest":
        RandomForestRegressor(
            random_state=RANDOM_STATE
        ),

    "Extra Trees":
        ExtraTreesRegressor(
            random_state=RANDOM_STATE
        ),

    "HistGradientBoosting":
        HistGradientBoostingRegressor(
            random_state=RANDOM_STATE
        ),

    "XGBoost":
        XGBRegressor(
            random_state=RANDOM_STATE
        )
}
Enter fullscreen mode Exit fullscreen mode

Cell 20 — Train and evaluate correctly

This cell is different from Notebooks 01 and 02.

The model predicts log-PCF.

We convert it back:

log-PCF prediction → expm1() → PCF prediction
Enter fullscreen mode Exit fullscreen mode

Then evaluate against original PCF.

# ============================================================
# 19. BASELINE MODEL EVALUATION
#
# Models are trained on log(PCF).
# Predictions are transformed back to the original PCF scale
# before calculating MAE, RMSE and R².
# ============================================================

baseline_results = []

for name, model in models.items():

    print(
        f"Training: {name}"
    )

    # Train using log-transformed target
    model.fit(
        X_train,
        y_train
    )

    # Predict log-PCF
    y_pred_log = model.predict(
        X_test
    )

    # Convert predictions back to original PCF scale
    y_pred = np.expm1(
        y_pred_log
    )

    # Prevent tiny numerical negative values
    y_pred = np.maximum(
        y_pred,
        0
    )

    # Evaluate on original PCF scale
    mae = mean_absolute_error(
        y_test_original,
        y_pred
    )

    rmse = np.sqrt(
        mean_squared_error(
            y_test_original,
            y_pred
        )
    )

    r2 = r2_score(
        y_test_original,
        y_pred
    )

    baseline_results.append({

        "Model": name,

        "MAE": mae,

        "RMSE": rmse,

        "": r2
    })
Enter fullscreen mode Exit fullscreen mode

Cell 21 — Baseline results

# ============================================================
# 20. BASELINE RESULTS
# ============================================================

baseline_results_df = pd.DataFrame(
    baseline_results
)

baseline_results_df = (
    baseline_results_df
    .sort_values(
        "",
        ascending=False
    )
    .reset_index(drop=True)
)

display(
    baseline_results_df.style.format({
        "MAE": "{:,.2f}",
        "RMSE": "{:,.2f}",
        "": "{:.4f}"
    })
)
Enter fullscreen mode Exit fullscreen mode

Cell 22 — Models for tuning

Same three as the other experiments.

# ============================================================
# 21. SELECT MODELS FOR HYPERPARAMETER TUNING
# ============================================================

tuning_models = {

    "Random Forest":
        RandomForestRegressor(
            random_state=RANDOM_STATE
        ),

    "Extra Trees":
        ExtraTreesRegressor(
            random_state=RANDOM_STATE
        ),

    "XGBoost":
        XGBRegressor(
            random_state=RANDOM_STATE
        )
}

print(
    "Models selected for tuning:"
)

for name in tuning_models:
    print("-", name)
Enter fullscreen mode Exit fullscreen mode

Cell 23 — RF GridSearch

# ============================================================
# 22. RANDOM FOREST — GRID SEARCH
# ============================================================

rf_param_grid = {

    "n_estimators": [
        100,
        200,
        300
    ],

    "max_depth": [
        None,
        10,
        20
    ],

    "min_samples_split": [
        2,
        5
    ],

    "min_samples_leaf": [
        1,
        2
    ]
}

rf_grid = GridSearchCV(
    estimator=tuning_models[
        "Random Forest"
    ],

    param_grid=rf_param_grid,

    scoring="neg_root_mean_squared_error",

    cv=5,

    n_jobs=-1,

    verbose=1
)

rf_grid.fit(
    X_train,
    y_train
)

print(
    "\nBest Random Forest parameters:"
)

print(
    rf_grid.best_params_
)
Enter fullscreen mode Exit fullscreen mode

Cell 24 — Tuned RF evaluation

Again, convert back to original PCF.

# ============================================================
# 23. EVALUATE TUNED RANDOM FOREST
# ============================================================

best_rf = rf_grid.best_estimator_

rf_pred_log = best_rf.predict(
    X_test
)

rf_pred = np.expm1(
    rf_pred_log
)

rf_pred = np.maximum(
    rf_pred,
    0
)

rf_mae = mean_absolute_error(
    y_test_original,
    rf_pred
)

rf_rmse = np.sqrt(
    mean_squared_error(
        y_test_original,
        rf_pred
    )
)

rf_r2 = r2_score(
    y_test_original,
    rf_pred
)

print("Random Forest")
print("MAE :", rf_mae)
print("RMSE:", rf_rmse)
print("R²  :", rf_r2)
Enter fullscreen mode Exit fullscreen mode

Cell 25 — Extra Trees GridSearch

# ============================================================
# 24. EXTRA TREES — GRID SEARCH
# ============================================================

et_param_grid = {

    "n_estimators": [
        100,
        200,
        300
    ],

    "max_depth": [
        None,
        10,
        20
    ],

    "min_samples_split": [
        2,
        5
    ],

    "min_samples_leaf": [
        1,
        2
    ]
}

et_grid = GridSearchCV(
    estimator=tuning_models[
        "Extra Trees"
    ],

    param_grid=et_param_grid,

    scoring="neg_root_mean_squared_error",

    cv=5,

    n_jobs=-1,

    verbose=1
)

et_grid.fit(
    X_train,
    y_train
)

print(
    "\nBest Extra Trees parameters:"
)

print(
    et_grid.best_params_
)
Enter fullscreen mode Exit fullscreen mode

Cell 26 — Tuned Extra Trees evaluation

# ============================================================
# 25. EVALUATE TUNED EXTRA TREES
# ============================================================

best_et = et_grid.best_estimator_

et_pred_log = best_et.predict(
    X_test
)

et_pred = np.expm1(
    et_pred_log
)

et_pred = np.maximum(
    et_pred,
    0
)

et_mae = mean_absolute_error(
    y_test_original,
    et_pred
)

et_rmse = np.sqrt(
    mean_squared_error(
        y_test_original,
        et_pred
    )
)

et_r2 = r2_score(
    y_test_original,
    et_pred
)

print("Extra Trees")
print("MAE :", et_mae)
print("RMSE:", et_rmse)
print("R²  :", et_r2)
Enter fullscreen mode Exit fullscreen mode

Cell 27 — XGBoost GridSearch

# ============================================================
# 26. XGBOOST — GRID SEARCH
# ============================================================

xgb_param_grid = {

    "n_estimators": [
        100,
        200,
        300
    ],

    "max_depth": [
        3,
        5,
        7
    ],

    "learning_rate": [
        0.01,
        0.05,
        0.1
    ],

    "subsample": [
        0.8,
        1.0
    ],

    "colsample_bytree": [
        0.8,
        1.0
    ]
}

xgb_grid = GridSearchCV(
    estimator=tuning_models[
        "XGBoost"
    ],

    param_grid=xgb_param_grid,

    scoring="neg_root_mean_squared_error",

    cv=5,

    n_jobs=-1,

    verbose=1
)

xgb_grid.fit(
    X_train,
    y_train
)

print(
    "\nBest XGBoost parameters:"
)

print(
    xgb_grid.best_params_
)
Enter fullscreen mode Exit fullscreen mode

Cell 28 — Tuned XGBoost evaluation

# ============================================================
# 27. EVALUATE TUNED XGBOOST
# ============================================================

best_xgb = xgb_grid.best_estimator_

xgb_pred_log = best_xgb.predict(
    X_test
)

xgb_pred = np.expm1(
    xgb_pred_log
)

xgb_pred = np.maximum(
    xgb_pred,
    0
)

xgb_mae = mean_absolute_error(
    y_test_original,
    xgb_pred
)

xgb_rmse = np.sqrt(
    mean_squared_error(
        y_test_original,
        xgb_pred
    )
)

xgb_r2 = r2_score(
    y_test_original,
    xgb_pred
)

print("XGBoost")
print("MAE :", xgb_mae)
print("RMSE:", xgb_rmse)
print("R²  :", xgb_r2)
Enter fullscreen mode Exit fullscreen mode

Cell 29 — Tuned comparison

# ============================================================
# 28. TUNED MODEL COMPARISON
# ============================================================

tuned_results_df = pd.DataFrame([

    {
        "Model": "Random Forest",
        "MAE": rf_mae,
        "RMSE": rf_rmse,
        "": rf_r2
    },

    {
        "Model": "Extra Trees",
        "MAE": et_mae,
        "RMSE": et_rmse,
        "": et_r2
    },

    {
        "Model": "XGBoost",
        "MAE": xgb_mae,
        "RMSE": xgb_rmse,
        "": xgb_r2
    }

])

tuned_results_df = (
    tuned_results_df
    .sort_values(
        "",
        ascending=False
    )
    .reset_index(drop=True)
)

display(
    tuned_results_df.style.format({
        "MAE": "{:,.2f}",
        "RMSE": "{:,.2f}",
        "": "{:.4f}"
    })
)
Enter fullscreen mode Exit fullscreen mode

Cell 30 — Baseline vs tuned

# ============================================================
# 29. BASELINE VS TUNED
# ============================================================

baseline_selected = (
    baseline_results_df[
        baseline_results_df["Model"].isin([
            "Random Forest",
            "Extra Trees",
            "XGBoost"
        ])
    ][
        ["Model", "MAE", "RMSE", ""]
    ]
    .copy()
)

baseline_selected["Stage"] = "Baseline"

tuned_comparison = tuned_results_df.copy()

tuned_comparison["Stage"] = "Tuned"

baseline_vs_tuned = pd.concat(
    [
        baseline_selected,
        tuned_comparison
    ],
    ignore_index=True
)

display(
    baseline_vs_tuned.sort_values(
        ["Model", "Stage"]
    )
)
Enter fullscreen mode Exit fullscreen mode

Cell 31 — Save baseline results

# ============================================================
# 30. SAVE BASELINE RESULTS
# ============================================================

baseline_results_df.to_csv(
    "baseline_log_target_pcf_results.csv",
    index=False
)

print(
    "Saved baseline results."
)
Enter fullscreen mode Exit fullscreen mode

Cell 32 — Save tuned results

# ============================================================
# 31. SAVE TUNED RESULTS
# ============================================================

tuned_results_df.to_csv(
    "tuned_log_target_pcf_results.csv",
    index=False
)

print(
    "Saved tuned results."
)
Enter fullscreen mode Exit fullscreen mode

Cell 33 — Save hyperparameters

# ============================================================
# 32. SAVE BEST HYPERPARAMETERS
# ============================================================

best_parameters_df = pd.DataFrame({

    "Model": [
        "Random Forest",
        "Extra Trees",
        "XGBoost"
    ],

    "Best Parameters": [
        rf_grid.best_params_,
        et_grid.best_params_,
        xgb_grid.best_params_
    ]
})

display(
    best_parameters_df
)

best_parameters_df.to_csv(
    "tuned_log_target_pcf_best_parameters.csv",
    index=False
)
Enter fullscreen mode Exit fullscreen mode

Cell 34 — Experiment summary

# ============================================================
# 33. EXPERIMENT SUMMARY
# ============================================================

print("=" * 80)
print("EXPERIMENT 03 — LOG TARGET PCF")
print("=" * 80)

print(
    "\nDataset rows:",
    len(df_ml)
)

print(
    "Training rows:",
    len(X_train)
)

print(
    "Testing rows:",
    len(X_test)
)

print(
    "\nTarget transformation: log1p(PCF)"
)

print(
    "Product Weight transformation: None"
)

best_baseline = baseline_results_df.iloc[0]

print(
    "\nBest baseline model by R²:"
)

print(
    best_baseline["Model"],
    "→ R² =",
    round(
        best_baseline[""],
        4
    )
)

best_tuned = tuned_results_df.iloc[0]

print(
    "\nBest tuned model by R²:"
)

print(
    best_tuned["Model"],
    "→ R² =",
    round(
        best_tuned[""],
        4
    )
)

print("\n" + "=" * 80)
Enter fullscreen mode Exit fullscreen mode

Cell 35 — Important experiment note

Don't fill this with guesses. Run the notebook first, then insert the actual values.

## Experiment Summary

This experiment investigated whether logarithmic transformation of the
PCF target improved prediction performance. The transformation was
motivated by the substantial positive skewness of the PCF distribution.

The same 866 observations, 80/20 train-test split, random state,
feature preprocessing, regression models and evaluation metrics used in
Experiments 01 and 02 were retained to support a controlled comparison.

The PCF target was transformed using `log1p(PCF)` before model training.
Product Weight remained in its original representation, subject only to
the established winsorisation procedure. Model predictions were
converted back to the original PCF scale using `expm1()` before
calculating MAE, RMSE and R².

The strongest baseline model achieved an R² of [XX], while the best
tuned model achieved an R² of [XX].

Compared with the raw-target experiment, the log-target transformation
produced [an improvement / a reduction / a mixed effect] in predictive
performance. These results are used to determine whether target
transformation should be retained in the final machine-learning
configuration.
Enter fullscreen mode Exit fullscreen mode

⚠️ One critical point about Notebook 03

There is a subtle issue we need to handle correctly.

In Notebook 03:

y_train = np.log1p(y_train_raw)
Enter fullscreen mode Exit fullscreen mode

means Company target encoding is also being fitted against log-PCF, because:

te.fit_transform(X_train, y_train)
Enter fullscreen mode Exit fullscreen mode

uses the transformed target.

That is logically consistent with a log-target experiment, but it means the experiment changes both the modelling target and the target encoding scale.

I do not want you to silently change this now, because your previous notebooks already use target encoding and we want methodological consistency.

However, when we write the dissertation methodology, we should explicitly document that the target encoding is fitted using the training target representation used by each experiment.


Your three notebooks are now cleanly defined

01_ML_Baseline_Raw_PCF.ipynb
    Raw PCF
    Raw Product Weight
             ↓
02_ML_Log_Product_Weight.ipynb
    Raw PCF
    Log Product Weight
             ↓
03_ML_Log_Target_PCF.ipynb
    Log PCF
    Raw Product Weight
Enter fullscreen mode Exit fullscreen mode

Run Notebook 03 through Cell 21 first and send me the baseline_results_df. We can then check the baseline before you spend time running all three GridSearchCV experiments.

Top comments (0)