Faza 02 · lecția 08

Ingineria și selecția caracteristicilor

Scopul lecției: Tip: Construire Limbaje: Python Cerințe preliminare: Faza 1 (Statistică pentru ML, Algebră liniară), Faza 2, lecțiile 1–7 Durată: ~90 de minute

Versiunea curentă AlexBred.com: primele 100 de lecții ale programului în limba română.

Curs
AI Engineering from Scratch
Fază
Bazele învățării automate
Lectură
24 min.
Verificat
Cuprinsul lecției
  1. Obiective de învățare
  2. Problema
  3. Conceptul
  4. Pipeline-ul caracteristicilor
  5. Caracteristici numerice
  6. Caracteristici categoriale
  7. Caracteristici textuale
  8. Valori lipsă
  9. Interacțiunea caracteristicilor
  10. Selecția caracteristicilor
  11. Construiți
  12. Pasul 1: transformări numerice de la zero
  13. Pasul 2: codificare categorială de la zero
  14. Pasul 3: caracteristici textuale de la zero
  15. Pasul 4: imputarea valorilor lipsă de la zero
  16. Pasul 5: selecția caracteristicilor de la zero
  17. Pasul 6: pipeline complet și demonstrație
  18. Folosiți
  19. Livrați
  20. Exerciții
  21. Termeni-cheie
  22. Lecturi suplimentare

O caracteristică bună poate valora cât o mie de puncte de date.

Tip: Construire Limbaje: Python Cerințe preliminare: Faza 1 (Statistică pentru ML, Algebră liniară), Faza 2, lecțiile 1–7 Durată: ~90 de minute

Obiective de învățare

  • Să implementați transformări numerice — standardizare, scalare min–max, transformare logaritmică și discretizare — și să explicați când este potrivită fiecare
  • Să construiți codificări one-hot, ordinale și bazate pe țintă pentru caracteristici categoriale și să identificați riscul de scurgere a datelor la codificarea prin țintă
  • Să construiți de la zero un vectorizator TF-IDF și să explicați când poate depăși numărarea brută a cuvintelor în clasificarea textului
  • Să aplicați selecția caracteristicilor prin metode de filtrare — prag de varianță, corelație și informație mutuală — pentru a reduce dimensionalitatea

Problema

Aveți un set de date. Alegeți un algoritm și îl antrenați. Rezultatele sunt mediocre. Încercați un algoritm mai sofisticat, dar rezultatele rămân mediocre. Petreceți o săptămână reglând hiperparametrii și obțineți doar o îmbunătățire marginală.

Apoi cineva transformă datele brute în caracteristici mai potrivite, iar o regresie logistică simplă depășește ansamblul gradient boosting pe care l-ați reglat.

Acest lucru se întâmplă frecvent. În ML clasic, reprezentarea datelor poate conta cel puțin la fel de mult ca alegerea algoritmului. Un model pentru prețul locuințelor care primește suprafața și numărul de dormitoare pornește de la un semnal mai util decât unul care primește adresa ca șir brut, indiferent de sofisticarea algoritmului. Modelul poate lucra numai cu informația și reprezentarea pe care i le furnizați.

Ingineria caracteristicilor transformă datele brute în reprezentări care fac tiparele mai accesibile modelului. Selecția caracteristicilor elimină caracteristici irelevante, redundante sau prea zgomotoase pentru obiectivul dat. Împreună pot avea un impact foarte mare în proiectele ML clasice, dar valoarea lor trebuie măsurată pe validare, în cadrul pipeline-ului complet.

Conceptul

Pipeline-ul caracteristicilor

Диаграмма к уроку «Ingineria și selecția caracteristicilor»

Toate etapele care învață statistici, categorii, vocabular sau selecții trebuie ajustate numai pe datele de antrenare din fiecare partiție de validare, apoi aplicate fără reajustare datelor de validare și testare.

Caracteristici numerice

Numerele brute necesită adesea transformări înainte de modelare:

Scalare: aduce caracteristicile la scări comparabile, astfel încât unitățile numerice să nu domine neintenționat algoritmii bazați pe distanță, precum K-Means și KNN, sau modelele sensibile la scară și regularizare, precum multe configurații SVM. Scalarea min–max ajustează valorile de antrenare la [0, 1]. Standardizarea z-score scade media de antrenare și împarte la abaterea standard de antrenare.

Notă tehnică: Datele noi pot ajunge în afara intervalului [0, 1] stabilit de MinMaxScaler, iar setul transformat nu va avea întotdeauna exact media 0 și abaterea 1. Parametrii se învață din train; relevanța scalării depinde de model și de semnificația unităților.

Transformare logaritmică: comprimă distribuțiile asimetrice la dreapta, precum venitul, populația sau numărul de cuvinte, și poate transforma unele relații multiplicative în relații aditive. Domeniul trebuie tratat explicit: log(1+x) necesită x > −1, iar valori mai mici sau semnate pot necesita o altă transformare.

Discretizare: transformă valori continue în intervale categoriale. Poate fi utilă când relația dintre caracteristică și țintă este neliniară, dar aproximativ în trepte, de exemplu pentru grupe de vârstă. Limitele intervalelor se învață din train, iar discretizarea pierde informație și introduce discontinuități.

Caracteristici polinomiale: creează termeni precum x^2, x^3 și x1*x2. Permit modelelor liniare să surprindă relații neliniare în intrările originale, cu prețul creșterii dimensionalității și al unui risc mai mare de supraînvățare.

Caracteristici categoriale

Multe modele necesită intrări numerice, deci categoriile trebuie codificate.

Codificare one-hot: creează o coloană binară pentru fiecare categorie cunoscută. „culoare = roșu/albastru/verde” devine trei coloane: este_roșu, este_albastru și este_verde. Funcționează bine pentru cardinalitate mică sau moderată, dar poate produce foarte multe coloane la cardinalitate mare. Stabiliți și comportamentul pentru categorii necunoscute.

Codificare ordinală: asociază fiecare categorie cu un întreg, de exemplu roșu=0, albastru=1, verde=2. Dacă ordinea nu are sens real, codurile introduc o relație artificială. Acest lucru afectează și arborii, deoarece o împărțire numerică grupează categoriile după pragul impus de coduri; codificarea este justificată direct atunci când categoriile sunt cu adevărat ordinale sau când estimatorul tratează explicit categoriile.

Codificare prin țintă: înlocuiește fiecare categorie cu o estimare netezită a mediei țintei pentru acea categorie. Este puternică, dar riscantă: pentru rândurile de antrenare trebuie folosită o schemă out-of-fold sau cross-fitting, astfel încât valoarea rândului să nu-și codifice propria țintă. Codificarea finală se ajustează numai pe train și se aplică pe test, cu o regulă pentru categorii necunoscute.

Caracteristici textuale

Vectorizator de numărare: numără aparițiile fiecărui termen într-un document. „pisica stă pe covor lângă pisică” devine {pisică: 2, stă: 1, pe: 1, covor: 1, lângă: 1} după tokenizarea și normalizarea alese.

TF-IDF: frecvența termenului înmulțită cu frecvența inversă a documentelor. Termenii sunt ponderați după cât de distinctivi sunt în corpus; cuvintele prezente în foarte multe documente primesc o pondere IDF mai mică.

TF(cuvânt, document) = numărul aparițiilor cuvântului în document / numărul total de cuvinte din document
IDF(cuvânt) = log(numărul total de documente / numărul documentelor care conțin cuvântul)
TF-IDF = TF * IDF

Aceasta este o formulă didactică. Implementările diferă prin tokenizare, netezirea IDF, adăugarea unei constante, TF subliniar și normalizarea vectorilor. TF-IDF poate depăși numărătorile brute când termenii foarte frecvenți sunt puțin discriminativi, dar nu există o garanție universală; comparați reprezentările prin validare.

Valori lipsă

Datele reale au goluri. Strategiile includ:

  • Eliminarea rândurilor: numai când pierderea de date și mecanismul lipsei sunt acceptabile; „rar și aleatoriu” trebuie verificat, nu presupus
  • Imputarea prin medie sau mediană: simplă; mediana este mai robustă la valori extreme, însă ambele modifică distribuția și reduc artificial varianța
  • Imputarea prin mod: pentru caracteristici categoriale, cu tratarea explicită a situațiilor fără valori observate
  • Coloană-indicator: adăugați o coloană binară precum „această_valoare_lipsea” înainte de imputare; lipsa poate fi ea însăși informativă
  • Propagare înainte sau înapoi: pentru serii temporale, respectând ordinea și informația disponibilă la momentul predicției; propagarea înapoi poate introduce informație din viitor

Strategia de imputare trebuie ajustată pe train în fiecare partiție de validare și aplicată ulterior fără a consulta distribuția setului de testare.

Interacțiunea caracteristicilor

Uneori relația se află în combinația dintre variabile. Înălțimea și greutatea separat pot fi mai puțin informative decât IMC = greutate / înălțime^2 pentru anumite obiective. Interacțiunile multiplică spațiul caracteristicilor, așa că folosiți cunoașterea domeniului, regularizarea și validarea pentru a le alege.

Selecția caracteristicilor

Mai multe caracteristici nu înseamnă întotdeauna performanță mai bună. Caracteristicile irelevante sau redundante pot crește costul, varianța și oportunitățile de supraînvățare.

Metode de filtrare, independente de estimator:

  • Corelație: identifică perechi cu dependență liniară puternică; eliminarea uneia este o alegere dependentă de obiectiv și nu surprinde redundanța neliniară
  • Informație mutuală: estimează câtă dependență există între o caracteristică și țintă; estimarea poate fi zgomotoasă și dependentă de discretizare sau de estimator
  • Prag de varianță: elimină caracteristici aproape constante; rezultatul depinde de scară și nu folosește ținta

Metode embedded și wrapper:

  • Regularizare L1, de exemplu Lasso pentru regresie sau logistică penalizată: metodă embedded care poate aduce ponderi exact la zero; nu identifică garantat „adevăratele” caracteristici și poate alege arbitrar între predictori corelați
  • Eliminare recursivă a caracteristicilor: metodă wrapper care antrenează modelul, elimină caracteristicile cu importanță mică și repetă

De ce contează selecția: într-un regim cu puține observații și multe caracteristici zgomotoase, un model cu zece caracteristici utile poate generaliza mai bine decât același model cu încă 90 irelevante. Diferența depinde însă de estimator, regularizare, dimensiunea eșantionului și procedura de selecție. Selecția trebuie inclusă în fiecare fold de validare pentru a evita o estimare optimistă.

feature-scaling

Construiți

Pasul 1: transformări numerice de la zero

import math


def min_max_scale(values):
    min_val = min(values)
    max_val = max(values)
    if max_val == min_val:
        return [0.0] * len(values)
    return [(v - min_val) / (max_val - min_val) for v in values]


def standardize(values):
    n = len(values)
    mean = sum(values) / n
    variance = sum((v - mean) ** 2 for v in values) / n
    std = math.sqrt(variance) if variance > 0 else 1.0
    return [(v - mean) / std for v in values]


def log_transform(values):
    return [math.log(v + 1) for v in values]


def bin_values(values, n_bins=5):
    min_val = min(values)
    max_val = max(values)
    bin_width = (max_val - min_val) / n_bins
    if bin_width == 0:
        return [0] * len(values)
    result = []
    for v in values:
        bin_idx = int((v - min_val) / bin_width)
        bin_idx = min(bin_idx, n_bins - 1)
        result.append(bin_idx)
    return result


def polynomial_features(row, degree=2):
    n = len(row)
    result = list(row)
    if degree >= 2:
        for i in range(n):
            result.append(row[i] ** 2)
        for i in range(n):
            for j in range(i + 1, n):
                result.append(row[i] * row[j])
    return result

Notă tehnică: Funcțiile de scalare și discretizare nu tratează listele vide și nu separă fit de transform. log_transform necesită valori mai mari decât −1, bin_values necesită n_bins > 0, iar polynomial_features generează numai termeni până la gradul 2 chiar dacă primiți degree > 2. Folosiți statisticile învățate din train pentru datele ulterioare.

Pasul 2: codificare categorială de la zero

def one_hot_encode(values):
    categories = sorted(set(values))
    cat_to_idx = {cat: i for i, cat in enumerate(categories)}
    n_cats = len(categories)

    encoded = []
    for v in values:
        row = [0] * n_cats
        row[cat_to_idx[v]] = 1
        encoded.append(row)

    return encoded, categories


def label_encode(values):
    categories = sorted(set(values))
    cat_to_int = {cat: i for i, cat in enumerate(categories)}
    return [cat_to_int[v] for v in values], cat_to_int


def target_encode(feature_values, target_values, smoothing=10):
    global_mean = sum(target_values) / len(target_values)

    category_stats = {}
    for feat, target in zip(feature_values, target_values):
        if feat not in category_stats:
            category_stats[feat] = {"sum": 0.0, "count": 0}
        category_stats[feat]["sum"] += target
        category_stats[feat]["count"] += 1

    encoding = {}
    for cat, stats in category_stats.items():
        cat_mean = stats["sum"] / stats["count"]
        weight = stats["count"] / (stats["count"] + smoothing)
        encoding[cat] = weight * cat_mean + (1 - weight) * global_mean

    return [encoding[v] for v in feature_values], encoding

Notă tehnică: Aceste funcții combină ajustarea și transformarea și nu definesc comportamentul pentru categorii noi. Mai important, target_encode codifică fiecare rând de antrenare cu o statistică ce include propria țintă; netezirea reduce, dar nu elimină scurgerea și supraînvățarea. Pentru train folosiți cross-fitting, iar pentru categorii noi folosiți o valoare de rezervă, de regulă media globală învățată.

Pasul 3: caracteristici textuale de la zero

def count_vectorize(documents):
    vocab = {}
    idx = 0
    for doc in documents:
        for word in doc.lower().split():
            if word not in vocab:
                vocab[word] = idx
                idx += 1

    vectors = []
    for doc in documents:
        vec = [0] * len(vocab)
        for word in doc.lower().split():
            vec[vocab[word]] += 1
        vectors.append(vec)

    return vectors, vocab


def tfidf(documents):
    n_docs = len(documents)

    vocab = {}
    idx = 0
    for doc in documents:
        for word in doc.lower().split():
            if word not in vocab:
                vocab[word] = idx
                idx += 1

    doc_freq = {}
    for doc in documents:
        seen = set()
        for word in doc.lower().split():
            if word not in seen:
                doc_freq[word] = doc_freq.get(word, 0) + 1
                seen.add(word)

    vectors = []
    for doc in documents:
        words = doc.lower().split()
        word_count = len(words)
        tf_map = {}
        for word in words:
            tf_map[word] = tf_map.get(word, 0) + 1

        vec = [0.0] * len(vocab)
        for word, count in tf_map.items():
            tf = count / word_count
            idf = math.log(n_docs / doc_freq[word])
            vec[vocab[word]] = tf * idf
        vectors.append(vec)

    return vectors, vocab

Notă tehnică: Tokenizarea prin lower().split() păstrează punctuația și este doar demonstrativă. Pentru un document gol, tfidf întoarce un vector nul deoarece bucla interioară nu se execută; nu împarte la zero. Implementarea nu netezește IDF, nu adaugă constanta folosită de multe biblioteci și nu normalizează vectorii. Vocabularul și IDF se învață numai pe train; termenii necunoscuți din date noi trebuie ignorați sau tratați printr-o politică stabilită.

Pasul 4: imputarea valorilor lipsă de la zero

def impute_mean(values):
    present = [v for v in values if v is not None]
    if not present:
        return [0.0] * len(values), 0.0
    mean = sum(present) / len(present)
    return [v if v is not None else mean for v in values], mean


def impute_median(values):
    present = sorted(v for v in values if v is not None)
    if not present:
        return [0.0] * len(values), 0.0
    n = len(present)
    if n % 2 == 0:
        median = (present[n // 2 - 1] + present[n // 2]) / 2
    else:
        median = present[n // 2]
    return [v if v is not None else median for v in values], median


def impute_mode(values):
    present = [v for v in values if v is not None]
    if not present:
        return values, None
    counts = {}
    for v in present:
        counts[v] = counts.get(v, 0) + 1
    mode = max(counts, key=counts.get)
    return [v if v is not None else mode for v in values], mode


def add_missing_indicator(values):
    return [0 if v is not None else 1 for v in values]

Funcțiile întorc atât valorile imputate, cât și statistica de completare, pentru ca aceasta să poată fi reutilizată. Totuși, interfața nu oferă o funcție transform separată, iar ramura fără nicio valoare observată alege convenții diferite: zero pentru numeric și valori None neschimbate pentru mod. Într-un pipeline de producție definiți explicit politica și învățați-o din train.

Pasul 5: selecția caracteristicilor de la zero

def correlation(x, y):
    n = len(x)
    mean_x = sum(x) / n
    mean_y = sum(y) / n
    cov = sum((xi - mean_x) * (yi - mean_y) for xi, yi in zip(x, y)) / n
    std_x = math.sqrt(sum((xi - mean_x) ** 2 for xi in x) / n)
    std_y = math.sqrt(sum((yi - mean_y) ** 2 for yi in y) / n)
    if std_x == 0 or std_y == 0:
        return 0.0
    return cov / (std_x * std_y)


def mutual_information(feature, target, n_bins=10):
    feat_min = min(feature)
    feat_max = max(feature)
    bin_width = (feat_max - feat_min) / n_bins if feat_max != feat_min else 1.0
    feat_binned = [
        min(int((f - feat_min) / bin_width), n_bins - 1) for f in feature
    ]

    n = len(feature)
    target_classes = sorted(set(target))

    feat_bins = sorted(set(feat_binned))
    p_feat = {}
    for b in feat_bins:
        p_feat[b] = feat_binned.count(b) / n

    p_target = {}
    for t in target_classes:
        p_target[t] = target.count(t) / n

    mi = 0.0
    for b in feat_bins:
        for t in target_classes:
            joint_count = sum(
                1 for fb, tv in zip(feat_binned, target) if fb == b and tv == t
            )
            p_joint = joint_count / n
            if p_joint > 0:
                mi += p_joint * math.log(p_joint / (p_feat[b] * p_target[t]))

    return mi


def variance_threshold(features, threshold=0.01):
    n_features = len(features[0])
    n_samples = len(features)
    selected = []

    for j in range(n_features):
        col = [features[i][j] for i in range(n_samples)]
        mean = sum(col) / n_samples
        var = sum((v - mean) ** 2 for v in col) / n_samples
        if var >= threshold:
            selected.append(j)

    return selected


def remove_correlated(features, threshold=0.9):
    n_features = len(features[0])
    n_samples = len(features)

    to_remove = set()
    for i in range(n_features):
        if i in to_remove:
            continue
        col_i = [features[r][i] for r in range(n_samples)]
        for j in range(i + 1, n_features):
            if j in to_remove:
                continue
            col_j = [features[r][j] for r in range(n_samples)]
            corr = abs(correlation(col_i, col_j))
            if corr >= threshold:
                to_remove.add(j)

    return [i for i in range(n_features) if i not in to_remove]

Notă tehnică: Codul presupune intrări nevide, rectangulare și cu lungimi compatibile. Estimarea informației mutuale este una empirică după discretizare egală și poate fi puternic influențată de n_bins și de eșantioane mici. remove_correlated este greedy și dependentă de ordinea coloanelor: elimină automat coloana cu indice mai mare fără să compare utilitatea pentru țintă.

Pasul 6: pipeline complet și demonstrație

import random


def make_housing_data(n=200, seed=42):
    random.seed(seed)
    data = []
    for _ in range(n):
        sqft = random.uniform(500, 5000)
        bedrooms = random.choice([1, 2, 3, 4, 5])
        age = random.uniform(0, 50)
        neighborhood = random.choice(["downtown", "suburbs", "rural"])
        has_pool = random.choice([True, False])

        sqft_with_missing = sqft if random.random() > 0.05 else None
        age_with_missing = age if random.random() > 0.08 else None

        price = (
            50 * sqft
            + 20000 * bedrooms
            - 1000 * age
            + (50000 if neighborhood == "downtown" else 10000 if neighborhood == "suburbs" else 0)
            + (15000 if has_pool else 0)
            + random.gauss(0, 20000)
        )

        data.append({
            "sqft": sqft_with_missing,
            "bedrooms": bedrooms,
            "age": age_with_missing,
            "neighborhood": neighborhood,
            "has_pool": has_pool,
            "price": price,
        })
    return data


if __name__ == "__main__":
    data = make_housing_data(200)

    print("=== Raw Data Sample ===")
    for row in data[:3]:
        print(f"  {row}")

    sqft_raw = [d["sqft"] for d in data]
    age_raw = [d["age"] for d in data]
    prices = [d["price"] for d in data]

    print("\n=== Missing Value Handling ===")
    sqft_missing = sum(1 for v in sqft_raw if v is None)
    age_missing = sum(1 for v in age_raw if v is None)
    print(f"  sqft missing: {sqft_missing}/{len(sqft_raw)}")
    print(f"  age missing: {age_missing}/{len(age_raw)}")

    sqft_indicator = add_missing_indicator(sqft_raw)
    age_indicator = add_missing_indicator(age_raw)
    sqft_imputed, sqft_fill = impute_median(sqft_raw)
    age_imputed, age_fill = impute_mean(age_raw)
    print(f"  sqft filled with median: {sqft_fill:.0f}")
    print(f"  age filled with mean: {age_fill:.1f}")

    print("\n=== Numerical Transforms ===")
    sqft_scaled = standardize(sqft_imputed)
    age_scaled = min_max_scale(age_imputed)
    sqft_log = log_transform(sqft_imputed)
    age_binned = bin_values(age_imputed, n_bins=5)
    print(f"  sqft standardized: mean={sum(sqft_scaled)/len(sqft_scaled):.4f}, std={math.sqrt(sum(v**2 for v in sqft_scaled)/len(sqft_scaled)):.4f}")
    print(f"  age min-max: [{min(age_scaled):.2f}, {max(age_scaled):.2f}]")
    print(f"  age bins: {sorted(set(age_binned))}")

    print("\n=== Categorical Encoding ===")
    neighborhoods = [d["neighborhood"] for d in data]

    ohe, ohe_cats = one_hot_encode(neighborhoods)
    print(f"  One-hot categories: {ohe_cats}")
    print(f"  Sample encoding: {neighborhoods[0]} -> {ohe[0]}")

    le, le_map = label_encode(neighborhoods)
    print(f"  Label encoding map: {le_map}")

    te, te_map = target_encode(neighborhoods, prices, smoothing=10)
    print(f"  Target encoding: {({k: round(v) for k, v in te_map.items()})}")

    print("\n=== Text Features ===")
    descriptions = [
        "large modern house with pool",
        "small cozy cottage near downtown",
        "spacious family home with large yard",
        "modern apartment downtown with view",
        "rustic cabin in rural area",
    ]
    cv, cv_vocab = count_vectorize(descriptions)
    print(f"  Vocabulary size: {len(cv_vocab)}")
    print(f"  Doc 0 non-zero features: {sum(1 for v in cv[0] if v > 0)}")

    tf, tf_vocab = tfidf(descriptions)
    print(f"  TF-IDF vocabulary size: {len(tf_vocab)}")
    top_words = sorted(tf_vocab.keys(), key=lambda w: tf[0][tf_vocab[w]], reverse=True)[:3]
    print(f"  Doc 0 top TF-IDF words: {top_words}")

    print("\n=== Polynomial Features ===")
    sample_row = [sqft_scaled[0], age_scaled[0]]
    poly = polynomial_features(sample_row, degree=2)
    print(f"  Input: {[round(v, 4) for v in sample_row]}")
    print(f"  Polynomial: {[round(v, 4) for v in poly]}")
    print(f"  Features: [x1, x2, x1^2, x2^2, x1*x2]")

    print("\n=== Feature Selection ===")
    feature_matrix = [
        [sqft_scaled[i], age_scaled[i], float(sqft_indicator[i]), float(age_indicator[i])]
        + ohe[i]
        for i in range(len(data))
    ]

    print(f"  Total features: {len(feature_matrix[0])}")

    surviving_var = variance_threshold(feature_matrix, threshold=0.01)
    print(f"  After variance threshold (0.01): {len(surviving_var)} features kept")

    surviving_corr = remove_correlated(feature_matrix, threshold=0.9)
    print(f"  After correlation filter (0.9): {len(surviving_corr)} features kept")

    binary_prices = [1 if p > sum(prices) / len(prices) else 0 for p in prices]
    print("\n  Mutual information with target:")
    feature_names = ["sqft", "age", "sqft_missing", "age_missing"] + [f"neigh_{c}" for c in ohe_cats]
    for j in range(len(feature_matrix[0])):
        col = [feature_matrix[i][j] for i in range(len(feature_matrix))]
        mi = mutual_information(col, binary_prices, n_bins=10)
        print(f"    {feature_names[j]}: MI={mi:.4f}")

    print("\n  Correlation with price:")
    for j in range(len(feature_matrix[0])):
        col = [feature_matrix[i][j] for i in range(len(feature_matrix))]
        corr = correlation(col, prices)
        print(f"    {feature_names[j]}: r={corr:.4f}")

Notă tehnică: Demonstrația este descriptivă și procesează întregul set simultan. Nu o folosiți ca șablon de evaluare: împărțiți mai întâi datele, apoi ajustați imputarea, scalarea, categoriile, target encoding și selecția numai în train sau în foldurile interne. În plus, calcularea binary_prices folosește media țintei pe toate datele și ar produce scurgere dacă ar intra într-o evaluare predictivă.

Folosiți

În scikit-learn, aceste transformări pot fi compuse în pipeline-uri:

from sklearn.preprocessing import StandardScaler, OneHotEncoder, PolynomialFeatures
from sklearn.impute import SimpleImputer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_selection import mutual_info_classif, VarianceThreshold
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline

numeric_pipe = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler()),
])

categorical_pipe = Pipeline([
    ("encoder", OneHotEncoder(sparse_output=False)),
])

preprocessor = ColumnTransformer([
    ("num", numeric_pipe, ["sqft", "age"]),
    ("cat", categorical_pipe, ["neighborhood"]),
])

Versiunile de la zero arată mecanica de bază a transformărilor. Implementările din bibliotecă adaugă tratarea cazurilor-limită, matrici rare, separarea fit/transform, compunerea pipeline-urilor și opțiuni suplimentare. Formulele nu sunt întotdeauna identice: de exemplu, TfidfVectorizer folosește implicit IDF netezit, un termen constant și normalizare L2.

Notă tehnică: OneHotEncoder(sparse_output=False) produce o matrice densă, potențial costisitoare la cardinalitate mare, și implicit ridică eroare pentru categorii necunoscute. Pentru producție luați în calcul ieșirea rară și o politică precum handle_unknown="ignore" sau gruparea categoriilor rare. Pipeline-ul prezentat este doar preprocesorul; pentru validare fără scurgere, includeți și estimatorul final în același Pipeline.

Livrați

Această lecție produce:

  • outputs/prompt-feature-engineer.md — un prompt pentru proiectarea sistematică a caracteristicilor din date brute

Exerciții

  1. Adăugați scalarea robustă, folosind mediana și intervalul intercuartilic în locul mediei și abaterii standard. Comparați-o cu standardizarea pe date cu valori extreme, ajustând ambele transformări numai pe train.
  2. Implementați target encoding leave-one-out: pentru fiecare rând de antrenare, calculați media țintei fără propria valoare. Arătați cum reduce supraînvățarea față de codificarea naivă. Comparați și cu cross-fitting pe mai multe folduri, care separă complet rândurile ce produc statistica de cele transformate.
  3. Construiți un pipeline automat de selecție care combină pragul de varianță, filtrarea corelațiilor și clasarea prin informație mutuală. Aplicați-l setului de locuințe și comparați regresia liniară cu toate caracteristicile față de cele selectate. Includeți întregul preprocessing și selecția în fiecare fold de validare, fără a consulta testul.

Termeni-cheie

Termen Cum i se spune Ce înseamnă de fapt
Ingineria caracteristicilor „Crearea unor coloane noi” Transformarea datelor brute în reprezentări care expun modelului tipare relevante
Standardizare „Aducerea la normal” Scăderea mediei de train și împărțirea la abaterea standard de train; valorile de train au media 0 și varianță unitară dacă abaterea este nenulă și se folosește convenția corespunzătoare
Codificare one-hot „Crearea variabilelor dummy” Câte o coloană binară pentru fiecare categorie cunoscută; un rând are de regulă un 1, iar categoriile necunoscute urmează politica encoderului
Codificare prin țintă „Folosirea răspunsului pentru codificare” Înlocuirea categoriei cu o estimare netezită bazată pe țintă, calculată out-of-fold pentru datele de antrenare
TF-IDF „Numărători sofisticate de cuvinte” Frecvența termenului înmulțită cu frecvența inversă a documentelor, eventual cu netezire și normalizare
Imputare „Completarea golurilor” Înlocuirea valorilor lipsă prin statistici sau predicții învățate numai din datele de antrenare
Selecția caracteristicilor „Eliminarea coloanelor slabe” Alegerea unui subset după relevanță, redundanță, stabilitate și cost, evaluat fără scurgere
Informație mutuală „Cât ne spune un lucru despre altul” Măsură a dependenței dintre variabile, echivalentă cu reducerea așteptată a incertitudinii despre Y prin observarea lui X
Scurgerea datelor „Trișare accidentală” Folosirea la ajustare a unei informații care nu ar fi disponibilă la momentul predicției, ceea ce produce rezultate prea optimiste

Lecturi suplimentare


Sursă: Feature Engineering & Selection — original

Navigare: înapoi: 02.07 — Învățare nesupravegheată · Faza 2 — Bazele învățării automate · Catalog complet · în continuare: 02.09 — Evaluarea modelelor.