Laborarbeit Künstliche Intelligenz¶

Thema : Bayes Netze und Case Based Reasoning¶

Namen des/r Studierenden:¶

Niklas Werthmann

Setup: Bibliotheken laden und Datensatz importieren¶

In [ ]:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix, ConfusionMatrixDisplay
import numpy as np
from pgmpy.models import BayesianNetwork
from pgmpy.factors.discrete import TabularCPD
from pgmpy.inference import VariableElimination

plt.set_loglevel("warning") # Setze den Log-Level für Matplotlib auf "warning"

# Einlesen der CSV-Datei
file_path = './bikedata/sampled_data_004.csv'
data = pd.read_csv(file_path, sep=";", decimal=",")

Datenbereinigung¶

Suche nach NaN Werten¶

In [75]:
print(data.isna().sum())   
Distance           0
Elapsed Time       0
Elevation High     0
Elevation Gain     0
Weekday            0
Bike Type         58
dtype: int64

Wir sehen, dass lediglich der Bike Type bei einigen Daten nicht bekannt ist. Da dies unsere Zielvariable ist, entfernen wir diese Datensätze.

In [76]:
data = data.dropna()

Explorative Datenanalyse¶

In [77]:
data.head()
Out[77]:
Distance Elapsed Time Elevation High Elevation Gain Weekday Bike Type
2 40.27 22875 2279.9 494.11 Saturday mtb
3 3.23 1233 539.0 21.00 Wednesday race bike
4 35.40 6199 371.0 157.00 Wednesday trecking bike
5 65.38 10591 530.2 937.00 Saturday race bike
6 52.38 7940 615.8 721.00 Saturday race bike
In [78]:
data.info()
<class 'pandas.core.frame.DataFrame'>
Index: 189 entries, 2 to 246
Data columns (total 6 columns):
 #   Column          Non-Null Count  Dtype  
---  ------          --------------  -----  
 0   Distance        189 non-null    float64
 1   Elapsed Time    189 non-null    int64  
 2   Elevation High  189 non-null    float64
 3   Elevation Gain  189 non-null    float64
 4   Weekday         189 non-null    object 
 5   Bike Type       189 non-null    object 
dtypes: float64(3), int64(1), object(2)
memory usage: 10.3+ KB
In [79]:
data.describe()
Out[79]:
Distance Elapsed Time Elevation High Elevation Gain
count 189.000000 189.000000 189.000000 189.000000
mean 40.362381 6947.867725 458.780423 412.484603
std 27.906235 5312.384989 322.351340 479.385288
min 3.230000 952.000000 -120.600000 13.800000
25% 28.300000 4697.000000 329.000000 146.000000
50% 35.250000 5666.000000 416.200000 279.000000
75% 41.890000 6810.000000 501.000000 517.000000
max 219.320000 45073.000000 2766.800000 4838.000000

Wir betrachten die Verteilung der Zielvariablen

In [80]:
print(data['Bike Type'].value_counts())
Bike Type
race bike        105
trecking bike     61
mtb               23
Name: count, dtype: int64

Umwandlung in %:

In [ ]:
print(data['Bike Type'].value_counts(normalize=True) * 100)
Bike Type
race bike        55.555556
trecking bike    32.275132
mtb              12.169312
Name: proportion, dtype: float64

Und die deskriptive Statistik der Daten für jeden Bike Type

In [82]:
grouped_data = data.groupby('Bike Type')
for bike_type, group in grouped_data:
    print(f"Descriptive statistics for {bike_type}:\n")
    print(group.describe())
    print("\n" + "-"*70 + "\n")
Descriptive statistics for mtb:

        Distance  Elapsed Time  Elevation High  Elevation Gain
count  23.000000     23.000000       23.000000       23.000000
mean   17.604348   4560.826087      384.743478      214.761739
std     9.200963   4421.963927      461.970185      121.670654
min     4.250000    952.000000     -120.600000       13.800000
25%    10.955000   2453.500000      170.400000      142.795000
50%    15.640000   3246.000000      359.000000      216.490000
75%    19.710000   4544.000000      413.300000      241.000000
max    40.270000  22875.000000     2279.900000      494.110000

----------------------------------------------------------------------

Descriptive statistics for race bike:

         Distance  Elapsed Time  Elevation High  Elevation Gain
count  105.000000    105.000000      105.000000      105.000000
mean    48.052571   7949.876190      542.256190      588.882000
std     32.171612   6251.371558      352.345925      567.584251
min      3.230000   1233.000000       12.400000       17.230000
25%     30.620000   4825.000000      454.800000      316.000000
50%     36.570000   5909.000000      497.600000      479.000000
75%     51.330000   8364.000000      525.200000      615.000000
max    219.320000  45073.000000     2766.800000     4838.000000

----------------------------------------------------------------------

Descriptive statistics for trecking bike:

         Distance  Elapsed Time  Elevation High  Elevation Gain
count   61.000000     61.000000       61.000000       61.000000
mean    35.706066   6123.131148      343.008197      183.400984
std     17.057845   2981.041532       64.974878      182.905175
min      4.090000   1851.000000       64.600000       48.200000
25%     34.890000   5374.000000      315.400000      123.000000
50%     35.340000   5719.000000      331.000000      144.000000
75%     35.890000   5999.000000      380.200000      161.000000
max    148.490000  21426.000000      543.000000     1444.000000

----------------------------------------------------------------------

Boxplots der numerischen Daten¶

In [83]:
# Numerische Attribute - einzelne Boxplots
numeric_columns = data.select_dtypes(include='number').columns

for column in numeric_columns:
    plt.figure(figsize=(8, 6))
    sns.boxplot(y=data[column])
    plt.title(f'Boxplot der numerischen Attributs: {column}')
    plt.ylabel(column)
    plt.grid(True)
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
In [84]:
# Boxplots zur Analyse von Bike Type vs. Leistungswerte
plt.figure(figsize=(12, 8))
for i, col in enumerate(['Distance', 'Elapsed Time', 'Elevation High', 'Elevation Gain'], 1):
    plt.subplot(2, 2, i)
    sns.boxplot(x=data['Bike Type'], y=data[col])
    plt.title(f'Bike Type vs. {col}')
    plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
No description has been provided for this image
In [85]:
# Boxplots zur Analyse von Weekday vs. Leistungswerte
plt.figure(figsize=(12, 8))
for i, col in enumerate(['Distance', 'Elapsed Time', 'Elevation High', 'Elevation Gain'], 1):
    plt.subplot(2, 2, i)
    sns.boxplot(x=data['Weekday'], y=data[col])
    plt.title(f'Weekday vs. {col}')
    plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
No description has been provided for this image

Häufigkeitsverteilungen der kategorialen Variablen¶

In [86]:
# Kategoriale Attribute - Häufigkeitsverteilungen
categorical_columns = data.select_dtypes(include='object').columns

for column in categorical_columns:
    plt.figure(figsize=(10, 5))
    data[column].value_counts().plot(kind='bar')
    plt.title(f'Häufigkeitsverteilung von {column}')
    plt.xlabel(column)
    plt.ylabel('Häufigkeit')
    plt.grid(True)
    plt.show()
No description has been provided for this image
No description has been provided for this image

Scatterplot von Elevation Gain und Distance nach Bike Type¶

In [87]:
# Scatterplot für Elevation Gain und Distance basierend auf Bike Type

plt.figure(figsize=(10, 6))
colors = {'race bike': 'red', 'mtb': 'green', 'trecking bike': 'yellow'}
sns.scatterplot(data=data, x='Elevation Gain', y='Distance', hue='Bike Type', palette=colors)
plt.title('Scatterplot von Elevation Gain vs Distance für Bike Type')
plt.xlabel('Elevation Gain')
plt.ylabel('Distance')
plt.grid(True)
plt.show()
No description has been provided for this image

Verteilung der Bike Types über die Wochentage¶

In [88]:
plt.figure(figsize=(10, 6))
sns.countplot(data=data, x='Weekday', hue='Bike Type')
plt.title('Anzahl der Fahrten pro Wochentag nach Bike Type')
plt.xlabel('Wochentag')
plt.ylabel('Anzahl')
plt.xticks(rotation=45)
plt.legend(title='Bike Type')
plt.show()
No description has been provided for this image

Korrelationsmatrix der numerischen Daten¶

In [89]:
correlation_matrix = data[['Distance', 'Elapsed Time', 'Elevation High', 'Elevation Gain']].corr()

plt.figure(figsize=(12, 8))
sns.heatmap(correlation_matrix, annot=True, fmt=".2f", cmap="coolwarm", cbar=True)
plt.title('Korrelationsmatrix der numerischen Attribute (Alle Daten)')
plt.show()
No description has been provided for this image

Korrelationsmatrix der numerischen Daten je Bike Type¶

In [90]:
# Group the data by 'Bike Type' and calculate the correlation matrix for each group
for bike_type, group in data.groupby('Bike Type'):
    correlation_matrix = group[['Distance', 'Elapsed Time', 'Elevation High', 'Elevation Gain']].corr()
    
    # Plot the heatmap for the correlation matrix
    plt.figure(figsize=(12, 8))
    sns.heatmap(correlation_matrix, annot=True, fmt=".2f", cmap="coolwarm", cbar=True)
    plt.title(f'Korrelationsmatrix der numerischen Attribute ({bike_type})')
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Aufgabenteil 1: Bayes Netze¶

Aufgabe: "Entwerfen Sie ein KI Modell auf Basis der Bayes Netze und setzen Sie dieses als Jupyter Notebook um. Insbesondere wird eine geeignete Netzarchitektur und ein Vorschlag für eine plausible Wahrscheinlichkeitsverteilung benötigt."

Wir beginnen mit der Definition des Bayes’schen Netzwerks und legen die kausalen Abhängigkeiten zwischen den Eingabemerkmalen und dem Zielmerkmal Bike Type fest.

1. Modell anlegen¶

In [91]:
model = BayesianNetwork([
    ('Distance', 'Bike Type'),
    ('Elapsed Time', 'Bike Type'),
    ('Elevation High', 'Bike Type'),
    ('Elevation Gain', 'Bike Type'),
    ('Weekday', 'Bike Type'),
])

Begründung zum Modell¶

Um die Struktur und Funktionsweise des Bayes’schen Netzwerks zunächst übersichtlich und nachvollziehbar zu halten, beschränken wir uns in einem ersten Schritt auf einen einzelnen Merkmalsknoten.
Diese Reduktion dient didaktischen Zwecken und ermöglicht es, den Einfluss eines einzelnen Merkmals isoliert zu betrachten sowie die Inferenzmechanismen des Netzwerks einfacher nachzuvollziehen.

2. CPTs anlegen¶

In diesem Abschnitt definieren wir die Conditional Probability Tables (CPTs), die das Herzstück unseres Bayes'schen Netzes bilden. Sie beschreiben die Wahrscheinlichkeitsverteilungen der Variablen – entweder marginal oder bedingt auf ihre Eltern im Netz. Grundlage für die Festlegung der CPTs sind deskriptive Statistiken der Daten sowie sinnvolle Schwellenwerte zur Diskretisierung. Bei geringer Datenverfügbarkeit greifen wir ergänzend auf fachliches Hintergrundwissen und realistische Annahmen zurück.

2.1 Elapsed Time¶

Basierend auf der deskriptiven Statistik:

Statistik Wert
Anzahl 189
Mittelwert ~6948
Standardabweichung ~5312
Minimum 952
25%-Perzentil 4697
Median 5666
75%-Perzentil 6810
Maximum 45073
Diskretisierungsregeln:¶
  • low: Elapsed Time ≤ 5666 (unterhalb des Medians)
  • medium: 5666 < Elapsed Time ≤ 6810 (zwischen Median und 75%)
  • high: Elapsed Time > 6810 (oberhalb des 75%-Perzentils)
Begründung:¶
  • low: Kurze, eventuell alltägliche Fahrten.
  • medium: Mittellange Touren.
  • high: Lange oder außergewöhnliche Fahrten.
Nächster Schritt:¶

Berechnung der Wahrscheinlichkeitsverteilung für die diskretisierten Werte.

In [92]:
print(pd.cut(data['Elapsed Time'],bins=[0, 5666, 6810, np.inf], labels=['low', 'medium', 'high']).value_counts(normalize=True).sort_index())
Elapsed Time
low       0.502646
medium    0.248677
high      0.248677
Name: proportion, dtype: float64
Erstellung der CPT für Elapsed Time¶
In [93]:
# Wir erhalten:
# low      ~0.5026
# medium   ~0.2487
# high     ~0.2487

# Dann ist die CPT wie folgt:
cpd_elapsed_time = TabularCPD(
    variable='Elapsed Time',
    variable_card=3,
    values=[
        [0.5026],    # P(low)
        [0.2487],   # P(medium)
        [0.2487]    # P(high)
    ],
    state_names={'Elapsed Time': ['low', 'medium', 'high']})

2.2 Distance¶

Basierend auf der deskriptiven Statistik:

Statistik Wert
Anzahl 189
Mittelwert ~40.36
Standardabweichung ~27.91
Minimum 3.23
25%-Perzentil 28.30
Median 35.25
75%-Perzentil 41.89
Maximum 219.32
Diskretisierungsregeln:¶
  • short: Distance ≤ 35.25 (unterhalb des Medians)
  • medium: 35.25 < Distance ≤ 41.89 (zwischen Median und 75%)
  • long: Distance > 41.89 (oberhalb des 75%-Perzentils)
Begründung:¶
  • short: Kürzere Strecken, die typischerweise alltägliche Fahrten darstellen.
  • medium: Mittellange Strecken, die zwischen alltäglichen und längeren Touren liegen.
  • long: Längere Strecken, die außergewöhnliche oder sportliche Fahrten repräsentieren.
Nächster Schritt:¶

Berechnung der Wahrscheinlichkeitsverteilung für die diskretisierten Werte.

In [94]:
print(pd.cut(data['Distance'], bins=[0, 35.25 , 41.89, np.inf], labels=['short', 'medium', 'long']).value_counts(normalize=True).sort_index())
Distance
short     0.502646
medium    0.248677
long      0.248677
Name: proportion, dtype: float64
Erstellung der CPT für Distance¶
In [95]:
# Wir erhalten:
# short      ~0.5026
# medium   ~0.2487
# long     ~0.2487

# Dann ist die CPT wie folgt:
cpd_distance = TabularCPD(
    variable='Distance',
    variable_card=3,
    values=[
        [0.5026],    # P(short)
        [0.2487],   # P(medium)
        [0.2487]    # P(long)
    ],
    state_names={'Distance': ['short', 'medium', 'long']}
)

2.3 Elevation High¶

Basierend auf der deskriptiven Statistik:

Statistik Wert
Anzahl 189
Mittelwert ~458.78
Standardabweichung ~322.35
Minimum -120.6
25%-Perzentil 329.0
Median 416.2
75%-Perzentil 501.0
Maximum 2766.8
Diskretisierungsregeln:¶
  • low: Elevation High ≤ 416.2 (unterhalb oder gleich dem Median)
  • high: Elevation High > 416.2 (oberhalb des Medians)
Begründung:¶
  • low: Niedrigere Höhenlagen, die typischerweise alltägliche oder weniger anspruchsvolle Fahrten darstellen.
  • high: Höhere Höhenlagen, die außergewöhnliche oder sportliche Fahrten repräsentieren.
Nächster Schritt:¶

Berechnung der Wahrscheinlichkeitsverteilung für die diskretisierten Werte.

In [96]:
print(pd.cut(data['Elevation High'], bins=[-np.inf, 416.2, np.inf], labels=['low', 'high']).value_counts(normalize=True).sort_index())
Elevation High
low     0.502646
high    0.497354
Name: proportion, dtype: float64
Erstellung der CPT für Elevation High¶
In [97]:
# Wir erhalten:
# low     ~0.5026
# high    ~0.4974

# Dann ist die CPT wie folgt:
cpd_elevation_high = TabularCPD(
    variable='Elevation High',
    variable_card=2,
    values=[
        [0.5026],  # P(low)
        [0.4974]   # P(high)
    ],
    state_names={'Elevation High': ['low', 'high']}
)

2.4 Elevantion Gain¶

Basierend auf der deskriptiven Statistik:

Statistik Wert
Anzahl 189
Mittelwert ~412.48
Standardabweichung ~479.39
Minimum 13.80
25%-Perzentil 146.00
Median 279.00
75%-Perzentil 517.00
Maximum 4838.00
Diskretisierungsregeln:¶
  • low: Elevation Gain ≤ 279 (unterhalb oder gleich dem Median)
  • medium: 279 < Elevation Gain ≤ 517 (zwischen Median und 75%-Perzentil)
  • high: Elevation Gain > 517 (oberhalb des 75%-Perzentils)
Begründung:¶
  • low: Geringe Höhengewinne, die typischerweise alltägliche oder weniger anspruchsvolle Fahrten darstellen.
  • medium: Mittlere Höhengewinne, die zwischen alltäglichen und sportlicheren Fahrten liegen.
  • high: Hohe Höhengewinne, die außergewöhnliche oder sportliche Fahrten repräsentieren.
Nächster Schritt:¶

Berechnung der Wahrscheinlichkeitsverteilung für die diskretisierten Werte.

In [98]:
print(pd.cut(data['Elevation Gain'], bins=[0, 279, 517, np.inf], labels=['low', 'medium', 'high']).value_counts(normalize=True).sort_index())
Elevation Gain
low       0.502646
medium    0.248677
high      0.248677
Name: proportion, dtype: float64
Erstellung der CPT für Elevation Gain¶
In [99]:
# Wir erhalten:
# low     ~0.5026
# medium  ~0.2487
# high    ~0.2487

# Dann ist die CPT wie folgt:

cpd_elevation_gain = TabularCPD(
    variable='Elevation Gain',
    variable_card=3,
    values=[
        [0.5026],   # P(low)
        [0.2487],   # P(medium)
        [0.2487]    # P(high)
    ],
    state_names={'Elevation Gain': ['low', 'medium', 'high']}
)

2.5 Weekday¶

Diskretisierungsregel:¶

Wir unterteilen Weekday in zwei Kategorien:

  • weekday: Monday to Friday
  • weekend: Saturday & Sunday
Begründung:¶

Die EDA zeigte, dass Race Bikes zwar unter der Woche bereits am häufigsten genutzt werden, jedoch ist ihr Anteil am Wochenende im Vergleich zu den anderen Bike Types noch ausgeprägter.

In [100]:
print(data['Weekday'].apply(lambda x: 'weekend' if x in ['Saturday', 'Sunday'] else 'weekday').value_counts(normalize=True).sort_index())
Weekday
weekday    0.698413
weekend    0.301587
Name: proportion, dtype: float64
Erstellung der CPT für Weekday¶
In [101]:
# Wir erhalten:
# weekday    ~0.6984
# weekend    ~0.3016

# Dann ist die CPT wie folgt:
cpd_weekday = TabularCPD(
    variable='Weekday',
    variable_card=2,
    values=[
        [0.6984],   # P(weekday)
        [0.3016]    # P(weekend)
    ],
    state_names={'Weekday': ['weekday', 'weekend']})

2.6 Biketype¶

Die Erstellung der CPT für Bike Type gestaltet sich komplexer.
Wir benötigen die diskretisierten Merkmale, um die bedingte Wahrscheinlichkeit der Zielvariable Bike Type zu berechnen.
Dazu erstellen wir eine Kopie des DataFrames und legen die diskretisierten Bereiche, wie zuvor definiert, fest.

In [102]:
# Wir erstellen eine neue DataFrame-Kopie mit allen diskretisierten Merkmalen
df_cpt = data.copy()

# Diskretisierung (wie oben schon verwendet)
df_cpt['Elapsed Time'] = pd.cut(df_cpt['Elapsed Time'], bins=[0, 5666, 6810, np.inf], labels=['low', 'medium', 'high'])
df_cpt['Distance'] = pd.cut(df_cpt['Distance'], bins=[0, 35.25, 41.89, np.inf], labels=['short', 'medium', 'long'])
df_cpt['Elevation High'] = pd.cut(df_cpt['Elevation High'], bins=[-np.inf, 416.2, np.inf], labels=['low', 'high'])
df_cpt['Elevation Gain'] = pd.cut(df_cpt['Elevation Gain'], bins=[0, 279, 517, np.inf], labels=['low', 'medium', 'high'])
df_cpt['Weekday'] = df_cpt['Weekday'].apply(lambda x: 'weekend' if x in ['Saturday', 'Sunday'] else 'weekday')

Nachdem die Diskretisierung abgeschlossen ist, gruppieren wir die Daten nach den diskretisierten Merkmalswerten und zählen, wie häufig jede Kombination dieser Merkmale für den jeweiligen Bike Type vorkommt.
Daraus entsteht eine Häufigkeitstabelle, die die Anzahl der Vorkommen jedes Bike Type für jede Merkmalskombination darstellt.

In [103]:
# Jetzt gruppieren wir nach allen Eltern und zählen die Bike Types
grouped = df_cpt.groupby(['Elapsed Time', 'Distance', 'Elevation High', 'Elevation Gain', 'Weekday', 'Bike Type'], observed=False).size()
grouped = grouped.unstack(fill_value=0)

Um die bedingte Wahrscheinlichkeit zu berechnen, teilen wir jede Häufigkeit durch die Gesamtsumme der Häufigkeiten für jede Kombination der Merkmale. Dies gibt uns die Wahrscheinlichkeiten, mit denen jeder Bike Type für jede Merkmalskombination auftritt.

In [104]:
cpt_bike_type = grouped.div(grouped.sum(axis=1), axis=0)

Nun liegen uns die bedingten Wahrscheinlichkeiten für den Bike Type in Bezug auf die Merkmale vor.

Bevor wir die Erstellung des CPT für die Zielvariable Bike Type abschließen können, müssen wir noch die Vollständigkeit prüfen, da es vorkommen kann, dass einige Kombinationen der Merkmale in den Daten fehlen.
In diesem Fall füllen wir die fehlenden Kombinationen auf, um sicherzustellen, dass das Bayes’sche Netzwerk alle möglichen Kombinationen der Merkmale berücksichtigen kann.

Wir definieren zunächst die Elternvariablen und die Anzahl der Zustände dieser:

In [ ]:
# Elternvariablen in folgender Reihenfolge:
# ['Distance', 'Elapsed Time', 'Elevation High', 'Elevation Gain', 'Weekday']

# Anzahl der Zustände je Variable
# Elapsed Time: 3 (low, medium, high)
# Distance: 3  (short, medium, long)
# Elevation High: 2 (low, high)
# Elevation Gain: 3 (low, medium, high)
# Weekday: 2 (weekday, weekend)

# Bike Type (Zielvariable): 3 Klassen -> ['mtb', 'race bike', 'trecking bike']

# Also:
evidence = ['Elapsed Time', 'Distance', 'Elevation High', 'Elevation Gain', 'Weekday']
evidence_card = [3, 3, 2, 3, 2]

Anschließend wird ein vollständiger Satz von Zuständen erzeugt, der alle möglichen Kombinationen der Merkmale umfasst. Fehlende Kombinationen werden mit den spezifischen Wahrscheinlichkeiten für die Bike Type-Kategorien aufgefüllt:

In [ ]:
# Sicherstellen, dass alle Kombinationen vorhanden sind

all_states = pd.MultiIndex.from_product([
    ['low', 'medium', 'high'],
    ['short', 'medium', 'long'],
    ['low', 'high'],
    ['low', 'medium', 'high'],
    ['weekday', 'weekend']
], names=evidence)

# Erinnerung (aus EDA) an die Verteilung (in %) der Bike Types:

# race bike        55.555556
# trecking bike    32.275132
# mtb              12.169312
                                        
# Auffüllen der fehlenden Werte mit den Wahrscheinlichkeiten
cpt_complete = cpt_bike_type.reindex(all_states).apply(
    lambda row: row.fillna(pd.Series([0.1217, 0.5556, 0.3228], index=row.index)),
    axis=1
)

Jetzt fehlt nur noch die Umwandlung der Wahrscheinlichkeiten für jede Bike Type-Klasse aus dem DataFrame cpt_complete in eine Liste von Listen, damit diese im richtigen Format für den TabularCPD-Prozess von pgmpy vorliegt.

In [107]:
# Klassen von Bike Type
bike_type_states = ['mtb', 'race bike', 'trecking bike']

# Extrahiere als Liste von Listen (je eine Liste pro Bike Type, transponiert für pgmpy)
cpt_values = [
    cpt_complete[bike_type].values.tolist() for bike_type in bike_type_states
]

Schließlich erstellen wir den CPT für die Zielvariable Bike Type basierend auf den berechneten Wahrscheinlichkeiten für jede Kombination der Merkmale.

In [108]:
cpd_bike_type = TabularCPD(
    variable='Bike Type',
    variable_card=3,
    values=cpt_values,
    evidence=evidence,
    evidence_card=evidence_card,
    state_names={
        'Bike Type': bike_type_states,
        'Elapsed Time': ['low', 'medium', 'high'],
        'Distance': ['short', 'medium', 'long'],
        'Elevation High': ['low', 'high'],
        'Elevation Gain': ['low', 'medium', 'high'],
        'Weekday': ['weekday', 'weekend']
    }
)

2.6 CPTs zum Modell hinzufügen¶

In [109]:
# Alle definierten CPTs in das Modell einfügen
model.add_cpds(
    cpd_distance,
    cpd_elapsed_time,
    cpd_elevation_high,
    cpd_elevation_gain,
    cpd_weekday,
    cpd_bike_type 
)

2.8 Modell überprüfen¶

In [110]:
# Prüfen, ob das Modell korrekt ist
assert model.check_model(), "Modell ist nicht konsistent!"
print("Bayes-Netzwerk erfolgreich aufgebaut und validiert.")
Bayes-Netzwerk erfolgreich aufgebaut und validiert.

3. Inferenz¶

Wir nutzen die Inferenz im Bayes-Netz zur Durchführung der Klassifikation unter Verwendung der Variablenelimination, um Schlussfolgerungen aus dem Netz basierend auf den Beobachtungen zu ziehen.
Dabei gibt es mehrere Verfahren, eines davon ist die Variablenelimination.

Zunächst betrachten wir eine einzelne Variable, ohne eine vorgelegte Evidenz.

In [111]:
# Inferenz-Objekt erstellen
infer = VariableElimination(model)

print(infer.query(['Distance']))
+------------------+-----------------+
| Distance         |   phi(Distance) |
+==================+=================+
| Distance(short)  |          0.5026 |
+------------------+-----------------+
| Distance(medium) |          0.2487 |
+------------------+-----------------+
| Distance(long)   |          0.2487 |
+------------------+-----------------+

Welche Wahrscheinlichkeiten hat Distance wenn es sich um ein Rennfahrrad handelt?

In [112]:
print(infer.query(['Distance'], evidence={'Bike Type': 'race bike'}))
+------------------+-----------------+
| Distance         |   phi(Distance) |
+==================+=================+
| Distance(short)  |          0.5139 |
+------------------+-----------------+
| Distance(medium) |          0.2160 |
+------------------+-----------------+
| Distance(long)   |          0.2701 |
+------------------+-----------------+

Wie hoch ist die Wahrscheinlichkeit der einzelnen Bike Type, wenn die folgenden Merkmalsausprägungen beobachtet werden?

In [113]:
# Beispiel
evidence_sample = {
    'Distance': 'long',
    'Elapsed Time': 'high',
    'Elevation High': 'high',
    'Elevation Gain': 'high',
    'Weekday': 'weekend'
}

# Inferenz durchführen
print(infer.query(variables=['Bike Type'], evidence=evidence_sample))
+--------------------------+------------------+
| Bike Type                |   phi(Bike Type) |
+==========================+==================+
| Bike Type(mtb)           |           0.0000 |
+--------------------------+------------------+
| Bike Type(race bike)     |           0.9412 |
+--------------------------+------------------+
| Bike Type(trecking bike) |           0.0588 |
+--------------------------+------------------+

Damit haben wir das Beispiel der diagnostischen Inferenz betrachtet, das für unsere Zwecke hier besonders gut geeignet ist.

4. Test und Bewertung¶

4.1 Test¶

In diesem Schritt wird die Diskretisierung der Testdaten gemäß der Modellstruktur durchgeführt.
Anschließend werden die Vorhersagen für den Fahrradtyp auf Basis der diskretisierten Merkmale berechnet. Die Inferenz wird für jedes
Beispiel im Testdatensatz durchgeführt, und die Vorhersage wird mit den tatsächlichen Labels verglichen, um die Modellleistung zu bewerten.

In [114]:
# Vorbereitung: Diskretisierung wie im Modell
test_data = df_cpt.copy()

# Liste zur Speicherung der Vorhersagen
predictions = []

for i, row in test_data.iterrows():
    evidence = {
        'Distance': row['Distance'],
        'Elapsed Time': row['Elapsed Time'],
        'Elevation High': row['Elevation High'],
        'Elevation Gain': row['Elevation Gain'],
        'Weekday': row['Weekday']
    }
    # Inferenz durchführen
    result = infer.query(variables=['Bike Type'], evidence=evidence)
    
    # Vorhersage: die Klasse mit der höchsten Wahrscheinlichkeit
    predicted_class = result.values.argmax()
    predicted_label = result.state_names['Bike Type'][predicted_class]
    predictions.append(predicted_label)

# Tatsächliche Labels
true_labels = test_data['Bike Type'].tolist()

# Bewertung
print("Accuracy:", accuracy_score(true_labels, predictions))
print("\nClassification Report:")
print(classification_report(true_labels, predictions))
Accuracy: 0.8095238095238095

Classification Report:
               precision    recall  f1-score   support

          mtb       0.75      0.13      0.22        23
    race bike       0.94      0.88      0.91       105
trecking bike       0.67      0.95      0.78        61

     accuracy                           0.81       189
    macro avg       0.79      0.65      0.64       189
 weighted avg       0.83      0.81      0.78       189

Zusätzlich wird eine Konfusionsmatrix erstellt, um die Vorhersageergebnisse zu visualisieren.

In [128]:
# Konfusionsmatrix berechnen
cm = confusion_matrix(true_labels, predictions, labels=bike_type_states)

# Konfusionsmatrix anzeigen
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=bike_type_states)
disp.plot(cmap='Blues', xticks_rotation=45)
plt.title("Konfusionsmatrix")
plt.show()
No description has been provided for this image

Wir erkennen, dass das trecking bike fast immer korrekt klassifiziert wird.
Das race bike wird in den meisten Fällen richtig erkannt, einige Fälle werden jedoch fälschlicherweise als trecking bike interpretiert.
Das größte Problem besteht beim mtb, da es fast nie korrekt klassifiziert wird und in der Mehrzahl der Fälle ebenfalls als trecking bike eingeordnet wird.

4.2 Diskussion der Ergebnisse¶

Die Vorhersagen des Bayes-Netzes wurden mit den tatsächlichen Labels verglichen, wobei die folgenden Metriken ermittelt wurden:

Metrik Wert
Accuracy 0.81
Precision (Macro Average) 0.79
Recall (Macro Average) 0.65
F1-Score (Macro Average) 0.64

Interpretation:

  • Das Modell erreicht eine insgesamt gute Genauigkeit (81 %), was bedeutet, dass die Mehrheit der Vorhersagen korrekt ist.
  • race bike werden mit sehr hoher Präzision (0.94) und Recall (0.88) erkannt – das Modell ist hier besonders zuverlässig.
  • trecking bike werden ebenfalls gut erkannt (Recall: 0.95), jedoch mit einer geringeren Präzision (0.67), was auf einige Verwechslungen mit anderen Klassen hinweist.
  • mtb bereiten dem Modell größere Probleme: Der Recall ist mit 0.13 sehr niedrig, was bedeutet, dass die meisten mtb fälschlich als andere Typen klassifiziert werden. Dies ist durch eine geringere Anzahl von Trainingsbeispielen und eine starke Ähnlichkeit zu den anderen Typen bedingt.

Fazit: Das Bayes-Netz bietet eine solide Grundlage zur Klassifikation von Fahrradtypen, insbesondere für Race Bikes. Für eine bessere Differenzierung der mtb-Klasse wäre es sinnvoll, eine feinere Diskretisierung oder eine alternative Modellstruktur zu erwägen.

Aufgabenteil 2: Case Based Reasoning¶

Aufgabe: "Entwerfen Sie ein KI Modell auf Basis von Case Based Reasoning und setzen Sie dieses als Jupyter Notebook um. Wählen Sie geeignete Vorgehensweisen um die Fallbasis aufzubauen (CBL) und geeignete Ähnlichkeitsmaße."

Zu Beginn diskretisieren wir unsere Daten (übernommen aus Aufgabe 1).

In [115]:
discretized_data = data[['Elapsed Time', 'Distance', 'Elevation High', 'Elevation Gain', 'Weekday', 'Bike Type']].copy()

# Diskretisierung der numerischen Spalten wie in Aufgabe 1
discretized_data['Elapsed Time'] = pd.cut(discretized_data['Elapsed Time'], bins=[0, 5666, 6810, np.inf], labels=['low', 'medium', 'high'])
discretized_data['Distance'] = pd.cut(discretized_data['Distance'], bins=[0, 35.25, 41.89, np.inf], labels=['short', 'medium', 'long'])
discretized_data['Elevation High'] = pd.cut(discretized_data['Elevation High'], bins=[-np.inf, 416.2, np.inf], labels=['low', 'high'])
discretized_data['Elevation Gain'] = pd.cut(discretized_data['Elevation Gain'], bins=[0, 279, 517, np.inf], labels=['low', 'medium', 'high'])
discretized_data['Weekday'] = discretized_data['Weekday'].apply(lambda x: 'weekend' if x in ['Saturday', 'Sunday'] else 'weekday')

1. Ähnlichkeitsmaß definieren¶

Im CBR-Ansatz geht man davon aus, dass ähnliche Fälle zu ähnlichen Ergebnissen (hier: Klassifikation des Bike Type) führen.
Da in unserem Fall alle relevanten Merkmale (wie Elapsed Time, Distance, Elevation High, Elevation Gain und Weekday) bereits diskretisiert vorliegen, verwenden wir einen gewichteten Matching-Koeffizienten:

  • Für jedes Merkmal prüfen wir, ob beide Fälle identisch sind.
  • Wir vergeben pro Übereinstimmung einen gewichteten Punktanteil und summieren alle Bewertungen.

Somit erhalten wir am Ende einen Ähnlichkeitswert zwischen 0 und 1.

Im folgenden Beispiel kommen unterschiedliche Gewichte zum Einsatz. Diese können – je nach Fachwissen und empirischer Validierung – angepasst werden.

In [116]:
def aehnlichkeit(fall1, fall2):
    """
    Berechnet die Ähnlichkeit zwischen zwei Fällen anhand diskreter Merkmalsausprägungen.
    Es wird ein gewichteter Matching-Koeffizient verwendet.

    Args:
        fall1 (dict): Ein Fall als Dictionary mit diskretisierten Merkmalen.
        fall2 (dict): Ein Fall als Dictionary mit diskretisierten Merkmalen.

    Returns:
        float: Ein Wert zwischen 0 und 1, der die Ähnlichkeit angibt.
    """

    # Merkmale, die für den Vergleich berücksichtigt werden sowie die Gewichte für die einzelnen Merkmale
    merkmale_und_gewichte = {
        'Elapsed Time': 0.5,
        'Distance': 0.25,
        'Elevation High': 0.25,
        'Elevation Gain': 0.25,
        'Weekday': 0.2
    }

    score = 0.0
    gesamtgewicht = sum(merkmale_und_gewichte.values())
    
    for merkmal, gewicht in merkmale_und_gewichte.items():
        # Falls beide Fälle im jeweiligen Merkmal übereinstimmen, wird das Gewicht addiert.
        if fall1.get(merkmal) == fall2.get(merkmal):
            score += gewicht

    # Normierung auf den Bereich [0,1]
    return score / gesamtgewicht

Test am Beispiel:

In [117]:
# Wir nutzen die zuvor diskretisierten Daten
fall1 = discretized_data.iloc[0].to_dict()
fall2 = discretized_data.iloc[1].to_dict()

print(fall1)
print(fall2)

print("Ähnlichkeit: ", aehnlichkeit(fall1, fall2))
{'Elapsed Time': 'high', 'Distance': 'medium', 'Elevation High': 'high', 'Elevation Gain': 'medium', 'Weekday': 'weekend', 'Bike Type': 'mtb'}
{'Elapsed Time': 'low', 'Distance': 'short', 'Elevation High': 'high', 'Elevation Gain': 'low', 'Weekday': 'weekday', 'Bike Type': 'race bike'}
Ähnlichkeit:  0.1724137931034483

Begründung zur Wahl des Ähnlichkeitsmaßes¶

  • Diskrete Merkmalsausprägungen: Da alle relevanten Merkmale diskretisiert wurden (analog zu Teil 1), bietet sich ein Matching-Koeffizient an, der per Feature prüft, ob die Ausprägungen übereinstimmen.
  • Gewichtete Kombination: Unterschiedliche Merkmale können unterschiedliche Relevanz besitzen. Mit gewichteter Aggregation lässt sich etwa abbilden, dass die Fahrzeit oder die Distanz stärker ins Gewicht fallen als der Wochentag.
  • Einfachheit und Interpretierbarkeit: Der Ansatz ist nachvollziehbar und ermöglicht eine einfache Anpassung der Gewichte je nach empirischer Evaluation.

2. CBR Zyklus umsetzen¶

Im Rahmen des CBR-Zyklus führen wir zunächst den RETRIEVE-Schritt durch, indem wir den Fall in der Fallbasis finden, der die größte Ähnlichkeit zum zu klassifizierenden Fall aufweist. Anschließend wird geprüft, ob die Ähnlichkeit einen vordefinierten Schwellenwert überschreitet. Falls ja, wird der Fall als ausreichend ähnlich angesehen und dessen Klassifikation übernommen. Andernfalls kann man den neuen Fall in die Fallbasis aufnehmen (RETAIN), um zukünftig ähnliche Fälle besser zu klassifizieren.

Die folgende Vergleichsfunktion finde_aehnlichsten_fall iteriert über die Fallbasis (hier ein DataFrame) und gibt den Index, den Ähnlichkeitswert sowie den zugehörigen Fall zurück, um den ähnlichsten Fall zu bestimmen.

In [ ]:
def finde_aehnlichsten_fall(df, person_zu_vergleichen):
    """
    Findet den Fall im DataFrame, der den höchsten Ähnlichkeitswert zur gegebenen Person hat.

    Args:
        df (pd.DataFrame): DataFrame mit den diskretisierten Fahrtdaten und zugehörigen Labels.
        person_zu_vergleichen (dict): Merkmalswerte des neuen Falls.

    Returns:
        tuple: (index, höchster Ähnlichkeitswert, Fall als Series)
    """
    # Initialisierung der Variablen
    max_aehnlichkeit = -1 # Der Wert max_aehnlichkeit wird mit -1 initialisiert, um sicherzustellen, dass jede berechnete Ähnlichkeit (die immer positiv ist) den Anfangswert überschreitet und der erste Vergleich immer wahr ist.
    index_aehnlichster_fall = -1 # Platzhalter für den Index des ähnlichsten Falls
    aehnlichster_datensatz = None

    for i, row in df.iterrows():
        fall = row.to_dict()
        # Berechnung der Ähnlichkeit basierend auf den definierten Merkmalen
        a = aehnlichkeit(person_zu_vergleichen, fall)
        if a > max_aehnlichkeit:
            max_aehnlichkeit = a
            index_aehnlichster_fall = i
            aehnlichster_datensatz = row

    return index_aehnlichster_fall, max_aehnlichkeit, aehnlichster_datensatz

Wir testen die Funktion mit fitiven Daten:

In [ ]:
fall_fiktiv = {'Elapsed Time': 'high', 'Distance': 'long', 'Elevation High': 'high',
            'Elevation Gain': 'medium', 'Weekday': 'weekday'}

idx, sim, retrieved = finde_aehnlichsten_fall(discretized_data, fall_fiktiv)
print("Index:", idx)
print("Ähnlichkeit:", sim)
print("Ähnlichster Datensatz:\n", retrieved)

bike_type = retrieved['Bike Type']
print(f"Daher sollte das Fahrrad vom Type {bike_type} sein.")
Index: 42
Ähnlichkeit: 1.0
Ähnlichster Datensatz:
 Elapsed Time           high
Distance               long
Elevation High         high
Elevation Gain       medium
Weekday             weekday
Bike Type         race bike
Name: 42, dtype: object
Daher sollte das Fahrrad vom Type race bike sein.

Erstellung einer Fallbasis (CBL Zyklus)

In [120]:
cases = pd.DataFrame(columns=['Elapsed Time', 'Distance', 'Elevation High', 'Elevation Gain', 'Weekday', 'Bike Type'])

Die Fallbasis ist zunächst leer. Diese wird nun nach dem CBL Algorithmus gefüllt. Vorgehen: Ein Fall wird aufgenommen, wenn er bisher nicht richtig klassifiziert wird.

In [121]:
# Füge den ersten Eintrag von data zu cases hinzu (initialisieren)
initial = discretized_data.iloc[0]
cases = pd.concat([cases, initial.to_frame().T], ignore_index=True)

Die folgende Schleife iteriert durch die discretized_data-Daten und führt für jede Zeile den Vergleich mit der Fallbasis durch, um den ähnlichsten Fall zu finden. Falsch klassifizierte Fälle werden in die Fallbasis cases aufgenommen.

In [ ]:
for index, row in discretized_data.iterrows():
    # Finde den ähnlichsten Fall in "cases"
    i, m, case = finde_aehnlichsten_fall(cases, row)

    # Vergleiche den Bike Type des gefundenen Falls mit dem Bike Type des aktuellen Falls
    if case['Bike Type'] != row['Bike Type']:
        # Füge den Datensatz zu "cases" hinzu
        cases = pd.concat([cases, row.to_frame().T], ignore_index=True)
In [123]:
print(f"Der Datensatz der Größe {len(discretized_data)} wurde in {len(cases)} Fällen gespeichert.")
Der Datensatz der Größe 189 wurde in 66 Fällen gespeichert.

Die Funktion treffer überprüft, wie oft die Funktion finde_aehnlichsten_fall den korrekten Fahrradtyp findet, indem sie für jedes Element im discretized_data-DataFrame den ähnlichsten Fall aus der Fallbasis cases sucht. Wenn der Fahrradtyp des gefundenen Falls mit dem der aktuellen Zeile übereinstimmt, wird die Anzahl der korrekten Übereinstimmungen erhöht. Fehlerhafte Übereinstimmungen werden mit einer Ausgabe des betroffenen Datensatzes angezeigt.

In [124]:
def treffer(discretized_data, cases):
    """
    Überprüft, wie oft finde_aehnlichsten_fall den korrekten Bike Type findet.

    Args:
        data: Der DataFrame, dessen Elemente geprüft werden sollen.
        cases: Der DataFrame, in dem nach ähnlichen Fällen gesucht wird.
        
    Returns:
        int: Anzahl der korrekten Übereinstimmungen.
    """

    korrekte_uebereinstimmungen = 0
    for index, row in discretized_data.iterrows():
        _, _, case = finde_aehnlichsten_fall(cases, row)
        if case['Bike Type'] == row['Bike Type']:
            korrekte_uebereinstimmungen += 1
        else:
            print(f"Fehler in Datensatz {index}")
            
    return korrekte_uebereinstimmungen

Aufruf der Funktion treffer mit den diskretisierten Daten und den Fällen

In [125]:
anzahl_korrekte_uebereinstimmungen = treffer(discretized_data, cases)

print("Anzahl korrekter Übereinstimmungen:", anzahl_korrekte_uebereinstimmungen)
Fehler in Datensatz 7
Fehler in Datensatz 10
Fehler in Datensatz 11
Fehler in Datensatz 16
Fehler in Datensatz 19
Fehler in Datensatz 30
Fehler in Datensatz 50
Fehler in Datensatz 53
Fehler in Datensatz 69
Fehler in Datensatz 75
Fehler in Datensatz 77
Fehler in Datensatz 79
Fehler in Datensatz 80
Fehler in Datensatz 86
Fehler in Datensatz 89
Fehler in Datensatz 94
Fehler in Datensatz 100
Fehler in Datensatz 102
Fehler in Datensatz 104
Fehler in Datensatz 107
Fehler in Datensatz 119
Fehler in Datensatz 124
Fehler in Datensatz 136
Fehler in Datensatz 139
Fehler in Datensatz 147
Fehler in Datensatz 150
Fehler in Datensatz 151
Fehler in Datensatz 162
Fehler in Datensatz 164
Fehler in Datensatz 166
Fehler in Datensatz 177
Fehler in Datensatz 178
Fehler in Datensatz 181
Fehler in Datensatz 182
Fehler in Datensatz 186
Fehler in Datensatz 189
Fehler in Datensatz 193
Fehler in Datensatz 196
Fehler in Datensatz 197
Fehler in Datensatz 198
Fehler in Datensatz 206
Fehler in Datensatz 208
Fehler in Datensatz 209
Fehler in Datensatz 212
Fehler in Datensatz 214
Fehler in Datensatz 220
Fehler in Datensatz 225
Fehler in Datensatz 226
Fehler in Datensatz 235
Fehler in Datensatz 246
Anzahl korrekter Übereinstimmungen: 139

3. Test und Bewertung¶

3.1 Test¶

Es erfolgt die Klassifikation des Bike Type anhand des ähnlichsten Falls aus der Fallbasis,
basierend auf den zuvor definierten, diskretisierten Testdaten.
Für jedes Beispiel im Testdatensatz wird der CBR-Zyklus durchlaufen, und die vorhergesagte Klasse wird mit dem tatsächlichen Label verglichen,
um die Leistungsfähigkeit des CBR-Modells zu evaluieren.

In [126]:
# 1. Vorhersagen erzeugen
y_true = []
y_pred = []

for index, row in discretized_data.iterrows():
    _, _, case = finde_aehnlichsten_fall(cases, row)
    y_true.append(row['Bike Type'])
    y_pred.append(case['Bike Type'])

# 2. Report erstellen
report = classification_report(y_true, y_pred)
print(report)
               precision    recall  f1-score   support

          mtb       0.28      0.30      0.29        23
    race bike       0.91      0.78      0.84       105
trecking bike       0.68      0.82      0.74        61

     accuracy                           0.74       189
    macro avg       0.62      0.63      0.62       189
 weighted avg       0.76      0.74      0.74       189

Zusätzlich wird eine Konfusionsmatrix erstellt, um die Vorhersageergebnisse zu visualisieren.

In [ ]:
cm = confusion_matrix(y_true, y_pred, labels=discretized_data['Bike Type'].unique())
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=discretized_data['Bike Type'].unique())
disp.plot(cmap='Blues', xticks_rotation=45)
plt.title("Confusion Matrix für CBR-Klassifikation")
plt.show()
No description has been provided for this image

trecking bike und race bike werden in den meisten Fällen korrekt klassifiziert, jedoch bereitet die mtb-Kategorie erneut erhebliche Probleme: Sie wird erneut nur seltenrichtig erkannt und meistens fälschlicherweise als trecking bike klassifiziert.

3.2 Diskussion der Ergebnisse¶

Die Vorhersagen des CBR-Klassifikators wurden mit den tatsächlichen Labels verglichen, wobei die folgenden Metriken ermittelt wurden:

Metrik Wert
Accuracy 0.74
Precision (Macro Average) 0.62
Recall (Macro Average) 0.63
F1-Score (Macro Average) 0.62

Interpretation:

  • Das Modell erreicht eine insgesamt ordentliche Genauigkeit (74 %), was bedeutet, dass etwa drei Viertel aller Vorhersagen korrekt sind.
  • race bike werden mit sehr hoher Präzision (0.91) und gutem Recall (0.78) erkannt – das Modell ist hier besonders zuverlässig.
  • trecking bike werden mit einem Recall von 0.82 ebenfalls gut erkannt. Die geringere Präzision (0.68) zeigt jedoch, dass es einige Verwechslungen mit anderen Fahrradtypen gibt.
  • mtb stellen die größte Herausforderung dar: Mit einer Precision von 0.28 und einem Recall von 0.30 erkennt das Modell diese Klasse nur unzureichend. Viele MTBs werden fälschlicherweise als andere Typen klassifiziert (haupsächlich als trecking bike).

Mögliche Ursachen:

  • Datenungleichgewicht: Die Kategorie mtb ist mit nur 23 von 189 Fällen unterrepräsentiert. Die geringe Anzahl an Beispielen erschwert dem CBR-Modell das zuverlässige Lernen typischer Merkmalskombinationen.
  • Ähnlichkeitsstruktur der Daten: In den verwendeten diskretisierten Merkmalen ähneln sich mtb und trecking bike, was zu Fehlklassifikationen führt.
  • Gewichtung der Merkmale: Die aktuell verwendeten Gewichte könnten für die Unterscheidung der mtb-Klasse weniger geeignet sein. Eine feinere Justierung oder lernbasierte Gewichtsanpassung könnte die Leistung verbessern.

Fazit: Das CBR-Modell liefert gute Ergebnisse für die dominante und gut unterscheidbare Klassen race bike und trecking bike. Die Leistung für die unterrepräsentierte Klasse mtb ist jedoch ungenügend. Eine Erweiterung der Fallbasis, eine Optimierung des Ähnlichkeitsmaßes oder eine Kombination mit anderen Ansätzen könnten die Klassifikationsleistung weiter verbessern.

Abschluss¶

Bayes-Netz¶

In diesem Schritt wurde die Klassifikation des Fahrradtyps auf Basis eines Bayes-Netzes getestet. Die diskretisierten Testdaten wurden als Evidenz verwendet, um für jedes Beispiel eine Wahrscheinlichkeitsverteilung über die möglichen Fahrradtypen zu berechnen. Die Vorhersage erfolgte jeweils durch Auswahl der Klasse mit der höchsten Wahrscheinlichkeit. Die tatsächlichen Labels wurden mit den Vorhersagen verglichen, um die Modellgüte zu bewerten.

Accuracy: 0.81

CBR-Modell¶

Das CBR-Modell klassifiziert jeden Fall basierend auf dem ähnlichsten bekannten Fall in der Fallbasis. Auch hier wurden die diskretisierten Testdaten verwendet, um ein Matching durchzuführen. Die tatsächlichen Labels wurden wiederum mit den Vorhersagen verglichen.

Accuracy: 0.74

Vergleich beider Modelle¶

Die folgende Tabelle fasst die wichtigsten Metriken zusammen:

Modell Accuracy Precision (Macro) Recall (Macro) F1-Score (Macro)
Bayes-Netz 0.81 0.79 0.65 0.64
CBR-Modell 0.74 0.62 0.63 0.62

Zur besseren Veranschaulichung wird nachfolgend ein Balkendiagramm dargestellt, das die wichtigsten Metriken der beiden Modelle gegenüberstellt.

In [ ]:
models = ['Bayes-Netz', 'CBR']

# Metriken für die beiden Modelle
accuracy = [0.81, 0.74]
precision = [0.79, 0.62]
recall = [0.65, 0.63]
f1_score = [0.64, 0.62]

x = np.arange(len(models))
width = 0.2

fig, ax = plt.subplots(figsize=(10, 6))

# Positionierung der Balken
x = np.arange(len(models))
width = 0.2

# Balken für jede Metrik hinzufügen
ax.barh(x - 1.5 * width, accuracy, width, label='Accuracy', color='skyblue')
ax.barh(x - 0.5 * width, precision, width, label='Precision (Macro)', color='orange')
ax.barh(x + 0.5 * width, recall, width, label='Recall (Macro)', color='green')
ax.barh(x + 1.5 * width, f1_score, width, label='F1-Score (Macro)', color='purple')

# Achsenbeschriftungen und Titel
ax.set_xlabel('Wert')
ax.set_title('Vergleich der Modelle nach Metriken')
ax.set_yticks(x)
ax.set_yticklabels(models)
ax.set_xlim(0, 1.05)
ax.legend()

# Gridlinien hinzufügen
plt.grid(axis='x', linestyle='--', alpha=0.5)
plt.tight_layout()
plt.show()
No description has been provided for this image

Fazit¶

Beide Modelle zeigen eine insgesamt solide Klassifikationsleistung. Auffällig ist, dass sich Accuracy, Recall und F1-Score bei beiden Ansätzen auf nahezu gleichem Niveau bewegen, während die Precision beim CBR-Modell deutlich geringer ausfällt.
Die Ursache für diesen Unterschied könnte in der Art der Fehlerverteilung liegen: Während das Bayes-Netz eine ausgewogenere Vorhersage trifft, neigt das CBR-Modell offenbar dazu, häufiger falsche positive Klassifikationen zu erzeugen – insbesondere bei der Klasse trecking bike. Dies erhöht zwar den Recall von trecking bike, senkt aber gleichzeitig die Precision des Modells.

Stärken des Bayes-Netzes:

  • Liefert konsistente und robuste Vorhersagen, insbesondere bei gut vertretenen Klassen wie race bike.
  • Die probabilistische Struktur ermöglicht eine differenzierte Entscheidungsfindung auf Basis der kombinierten Evidenz.

Stärken des CBR-Modells:

  • Vergleichbare Gesamtleistung trotz einfacherer Struktur.
  • Sehr gut interpretierbar und flexibel erweiterbar durch neue Fälle.
  • Besonders effektiv bei Klassen mit eindeutigen Merkmalsprofilen (z. B. race bike), zeigt jedoch Schwächen bei der Differenzierung ähnlicher bzw. unterrepräsentierter Klassen wie mtb.

Ausblick: Zur weiteren Optimierung beider Ansätze bieten sich spezifische Verbesserungsmöglichkeiten an.
Für das Bayes-Netz könnte eine feinere Diskretisierung sowie eine Erweiterung der CPT-Struktur die Modellleistung weiter steigern, insbesondere im Hinblick auf seltenere Merkmalskombinationen.
Das CBR-Modell hingegen könnte durch eine gezielte Gewichtsanpassung im Ähnlichkeitsmaß profitieren. Zudem wäre eine bessere Repräsentation seltener Klassen wünschenswert, um die Klassifikation unterrepräsentierter Typen wie mtb zu verbessern.