Inhaltsverzeichnis¶
Wir schausen sowohl die deutsche als auch die nordamerikanische Alterskennzeichnung an.
- Hintergrundwissen
- Datenbeschreibung / Visualisierung
- Trainieren und Testen des Eintscheidungsbaum-Modells
🇩🇪 Unterhaltungssoftware Selbstkontrolle (USK)¶
Die USK ist die deutsche Altersfreigabe für Computerspiele. Sie wurde im Jahr 1994 gegründet und diente ursprünglich als Empfehlung für Erziehungsberechtigte und als Orientierungshilfe für den Handel.
Spiele werden durchgespiel und auf ihre Inhalte hin bewertet. Die USK prüft Spiele auf Gewaltdarstellung, Sexualität, Drogenkonsum und andere jugendschutzrelevante Aspekte.
Im Jahr 2003 wurde Altersbewertung von Computerspielen in Deutschland verpflichtend.
Nach § 14 Absatz 2 des Jugendschutzgesetzes (JuSchG) werden Spielprogramme in 5 Altersstufen eingeteilt:
- ohne Altersbeschränkung (0)
- ab 6 Jahren
- ab 12 Jahren
- ab 16 Jahren
- keine Jugendfreigabe (ab 18 Jahren).
Datenerhebung¶
Die Daten wurden von der USK-Website (https://usk.de) erhoben. Die Altersfreigaben und die zugehörigen Altersstufen wurden aus der Seite extrahiert. Die Daten umfassen auch verschiedene Deskriptoren, die auf die Inhalte der Spiele hinweisen.
Fragestellung¶
Können wir die Alterfreigabe von Spielprogrammen vorhersagen, wenn wir die Spielinhalte kennen?
Entscheidungsbaum¶
Von André Flöter - eigene Graphik, PD-Schöpfungshöhe, https://de.wikipedia.org/w/index.php?curid=4774530
Ein Entscheidungsbaum ist ein Klassifikationsalgorithmus, der die Daten in Form eines Baums darstellt. Jeder Knoten im Baum repräsentiert eine Entscheidung basierend auf einem Feature. Da die Kategorien binär sind, können wir den Baum in Form eines Entscheidungsbaums darstellen.
Die Inhalte eines Spiels können in Form von Features dargestellt werden. Die Features sind effektiv Kategorien, die das Spiel beschreiben. Also hierbei handelt es sich um kategorische Daten.
Preprocessing¶
Ursprünglich werden Inhaltskennzeichnungen mit Kommas getreennt. Wir wandeln diese in unterschiedliche Spalten um.
title_name | age_rating | Düstere Atmosphäre | Angedeutete Gewalt | Schimpfwörter | Comic-Gewalt | Drastische Gewalt | Sexualisierte Gewalt | Drogen | Glücksspielthematik | ... | Erhöhte Kommunikationsrisiken | Sexualisierte Rollenbilder | Erhöhte Kaufanreize | Standortweitergabe | Sexuelle Andeutungen | Sexuelle Inhalte | Gewalt | Chats | In-Game-Käufe | In-Game-Käufe + zufällige Objekte | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Lonely Mountains: Snow Riders | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
1 | Wobbly Life | 6 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2 | Nicktoons & The Dice of Destiny | 12 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
3 | United Assault – Final Stand | 16 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
4 | High On Life | 18 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
5 rows × 31 columns
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, confusion_matrix
from sklearn import tree
class_names_ordered = ["0", "6", "12", "16", "18"]
columns = df.columns[10:]
ratings = df["age_rating"]
features = df[columns]
features_train, features_test, ratings_train, ratings_test = train_test_split(features, ratings, test_size=0.2, random_state=42)
print("[", ", ".join(columns.tolist()[0:15]), "...", "]")
print("\nNumbers of different features:", len(columns.to_list()))
print("Training set size:", len(features_train))
print("Test set size:", len(features_test))
[ Düstere Atmosphäre, Angedeutete Gewalt, Schimpfwörter, Comic-Gewalt, Drastische Gewalt, Sexualisierte Gewalt, Drogen, Glücksspielthematik, Belastende Themen, Handlungsdruck, Schreckmomente, Horror, Alkohol, Druck zum Vielspielen, Problematische Werbeinhalte ... ] Numbers of different features: 29 Training set size: 34418 Test set size: 8605
# Training initial decision tree
clf = tree.DecisionTreeClassifier()
clf.fit(features_train, ratings_train)
DecisionTreeClassifier()In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
DecisionTreeClassifier()
ratings_train_pred = clf.predict(features_train)
ratings_test_pred = clf.predict(features_test)
show_accuracy_plots(ratings_train, ratings_train_pred, ratings_test, ratings_test_pred)
Was ist hier schief gelaufen?¶
Einige Titel haben keine Inhaltsbeschreibungen, aber dennoch eine Altersfreigabe höhe als 0. Das ist ein Problem, da die Altersfreigabe auf den Inhalten basieren sollte. Wir wollen herausfinden, ob wir diese Titel mit einem Entscheidungsbaum klassifizieren können.
Preprocessing Teil 2¶
Lass uns die Daten etwa sauberer machen. Wir entfernen Titel, die eine Altersfreigabe höher als 0 haben, aber keine Inhaltsbeschreibungen. Das ist ein Hinweis darauf, dass die Daten nicht vollständig sind.
Was bleibt?
- Titel mit Altersfreigabe 0, die keine Inhaltsbeschreibungen haben
- Titel mit Altersfreigabe 0, die Inhaltsbeschreibungen haben
- Titel mit Altersfreigabe höher als 0, die Inhaltsbeschreibungen haben
# Filters out rating higher than 0 with no descriptors
total_descriptors = df[columns].sum(axis=1)
df = df[~((df["age_rating"] != "0") & (total_descriptors == 0))]
print("Filtered dataset size:", len(df))
ratings = df["age_rating"]
features = df[columns]
features_train, features_test, ratings_train, ratings_test = train_test_split(features, ratings, test_size=0.2, random_state=42)
Filtered dataset size: 20249
Das sieht viel besser aus!!¶
Wir schauen zunächst wie der Baum aussieht:
Ein großer Baum¶
Der Baum ist aktuell sehr complex und groß. Vielleicht können wir ihn vereinfachen und die Genauigkeit trotzdem hoch halten.
tree.plot_tree(clf, class_names=class_names_ordered, filled=True)
plt.show()
print("Numbers of leaves:", clf.get_n_leaves())
print("Numbers of nodes:", clf.tree_.node_count)
Numbers of leaves: 134 Numbers of nodes: 267
Die Anzahl der Blätter und Knoten begrenzen¶
Wir können harte Grenzen für die Anzahl der Blätter und Knoten im Baum setzen.
clf = tree.DecisionTreeClassifier(max_leaf_nodes=25, max_depth=10)
Wir müssen wir aber manuell die besten Werte für max_leaf_nodes
und max_depth
finden.
Cost Complexity Pruning und Alpha-Wert¶
Je tiefer der Baum, entstehen desto mehr Kosten. Wir rechnen hier mit Hilfe der Funtion DecisionTreeClassifier.cost_complexity_pruning_path()
einen alpha-Wert aus, der die Kosten des Baums repräsentiert. Je höher der Wert, desto weniger komplex wird der Baum.
Overfitting¶
Overfitting ist ein häufiges Problem bei Entscheidungsbäumen, insbesondere wenn sie zu tief sind. Ein zu tiefer Baum kann die Trainingsdaten zu gut lernen und generalisiert nicht gut auf neue Daten. Man erkennt Overfitting daran, dass die Trainingsgenauigkeit viel höher ist als die Testgenauigkeit.
Bei uns ist hier aber kein Overfitting zu erkennen, da die Trainings- und Testgenauigkeit sehr ähnlich sind. Wir können aber trotzdem den Baum vereinfachen, um die Interpretierbarkeit zu erhöhen.
Wir plotten zuerst die Aplha-Werte und die daraus entstehede Anzahl der Knoten:
# Pruning mit cost complexity pruning
path = clf.cost_complexity_pruning_path(features_train, ratings_train)
ccp_alphas = path.ccp_alphas
ccp_alphas = [alpha for alpha in ccp_alphas if alpha >= 0.0003 and alpha <= 0.03]
clfs = []
ccp_alphas = [alpha for alpha in ccp_alphas if alpha >= 0.0003 and alpha <= 0.03]
for ccp_alpha in ccp_alphas:
clf = tree.DecisionTreeClassifier(random_state=0, ccp_alpha=ccp_alpha)
clf.fit(features_train, ratings_train)
clfs.append(clf)
node_counts = [clf.tree_.node_count for clf in clfs]
depth = [clf.tree_.max_depth for clf in clfs]
Wir nehmen hier 0.004 als Alpha-Wert, da wir hier eine gute Balance zwischen Genauigkeit und Komplexität finden. Eine höhere Alpha-Wert bringt begrenzte Reduktion der Knotenzahl. Eine niedrigere Alpha-Wert würde den Baum zu komplex machen.
clf = tree.DecisionTreeClassifier(random_state=0,
ccp_alpha=0.004) # <-
clf.fit(features_train, ratings_train)
ratings_train_pred = clf.predict(features_train)
ratings_test_pred = clf.predict(features_test)
show_accuracy_plots(ratings_train, ratings_train_pred, ratings_test, ratings_test_pred)
print("Numbers of leaves:", clf.get_n_leaves(), "Numbers of nodes:", clf.tree_.node_count)
Numbers of leaves: 9 Numbers of nodes: 17
Die Anzahl der Knoten ist jetzt deutlich geringer (267 -> 17). Einige titel werden aber falsch als 0 klassifiziert. Gibt es eine Möglichkeit, das zu verhindern?
Wir fügen zuerst die Anzahl der Deskriptoren zu den Features hinzu.
number_of_descriptors = df.iloc[:, 10:].sum(axis=1)
df.insert(10, "number_of_descriptors", number_of_descriptors)
clf = tree.DecisionTreeClassifier(random_state=0, ccp_alpha=0.004)
clf.fit(features_train, ratings_train)
ratings_train_pred = clf.predict(features_train)
ratings_test_pred = clf.predict(features_test)
show_accuracy_plots(ratings_train, ratings_train_pred, ratings_test, ratings_test_pred)
print("Numbers of leaves:", clf.get_n_leaves(), "\nNumbers of nodes:", clf.tree_.node_count)
Numbers of leaves: 5 Numbers of nodes: 9
Class Weights¶
Jetzt werden 6er Titel gar nicht berücksichtigt. Das liegt daran, dass die 6er Titel relativ selten im Vergleich zu 0er Titel sind. Wir können das Problem mit Class Weights lösen.
clf = tree.DecisionTreeClassifier(random_state=0, ccp_alpha=0.004,
class_weight="balanced") # <-
clf.fit(features_train, ratings_train)
ratings_train_pred = clf.predict(features_train)
ratings_test_pred = clf.predict(features_test)
show_accuracy_plots(ratings_train, ratings_train_pred, ratings_test, ratings_test_pred)
print("Numbers of leaves:", clf.get_n_leaves(), "\nNumbers of nodes:", clf.tree_.node_count)
Numbers of leaves: 12 Numbers of nodes: 23
Wir haben die Anzahl der Blätter und Knoten im Baum um fast 10x reduziert, tortzdem verlieren wir im Test nur 1.1% der Genaulichkeit!!¶
fig, ax = plt.subplots(figsize=(22, 10))
tree.plot_tree(clf, filled=True, class_names=class_names_ordered, feature_names=columns.tolist(), label="all")
plt.show()
Bessere Rechnen der Genauigkeit¶
Wenn die geschätzte und die tatsächliche Altersfreigabe zu weit auseinander liegen, sollte das harter bestraft werden. Wir können das mit einer Distanzmatrix erreichen, die die Differenz zwischen den Altersfreigaben gewichtet.
Distanzmatirx¶
show_accuracy_plots(ratings_train, ratings_train_pred, ratings_test, ratings_test_pred, True)
Total weighted error: 0.9319326959113436 Max weighted error: 19.999999999999996 Total weighted error: 1.091355960564243 Max weighted error: 20.0
🇺🇸 + 🇨🇦 Entertainment Software Rating Board (ESRB)¶
Die ESRB ist das nordamerikanische Altersfreigabesystem für Computerspiele. Sie wurde im Jahr 1994 gegründet und dient als Orientierungshilfe für Eltern sowie als Richtlinie für den Handel in den USA und Kanada. Spiele werden von der ESRB anhand von Videomaterial und schriftlichen Informationen bewertet, wobei auf Inhalte wie Gewaltdarstellung, Sexualität, Drogenkonsum und andere jugendschutzrelevante Aspekte geachtet wird.
Die Altersbewertung durch die ESRB ist in Nordamerika für die meisten großen Publisher und Händler verpflichtend. Die Spiele werden in verschiedene Altersstufen eingeteilt:
- Everyone (E, für alle Altersgruppen)
- Everyone 10+ (E10+, ab 10 Jahren), erst im Jahr 2005 eingeführt
- Teen (T, ab 13 Jahren)
- Mature (M, ab 17 Jahren)
- Adults Only (AO, ab 18 Jahren)
Datensatz¶
Genutzt wird ein auf Kaggle bereitgestellter Datensatz vom Nutzer asherkatz. Der Datensatz ist bereits vorbereitet und Inhaltslabels sind auch separiert.
Entscheidungsbaum (diesmal mit ESRB)¶
columns = df.columns.difference([df.columns[-2], df.columns[-7]])[0:-17]
ratings = df["esrb_rating"]
class_names_ordered = ["E", "E10+", "T", "M", "AO"]
features = df[columns]
features_train, features_test, ratings_train, ratings_test = train_test_split(features, ratings, test_size=0.2, random_state=42)
print("[", ", ".join(columns.tolist()[0:15]), "...", "]")
print("\nNumbers of different features:", len(columns.to_list()))
print("Training set size:", len(features_train))
print("Test set size:", len(features_test))
[ Alcohol Reference, Alcohol and Tobacco Reference, Animated Blood, Animated Blood and Gore, Animated Violence, Blood, Blood and Gore, Cartoon Violence, Comic Mischief, Crude Humor, Drug Reference, Drug and Alcohol Reference, Edutainment, Fantasy Violence, Gambling ... ] Numbers of different features: 56 Training set size: 36712 Test set size: 9179
# Training initial decision tree
clf = tree.DecisionTreeClassifier()
clf.fit(features_train, ratings_train)
DecisionTreeClassifier()In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
DecisionTreeClassifier()
ratings_train_pred = clf.predict(features_train)
ratings_test_pred = clf.predict(features_test)
show_accuracy_plots(ratings_train, ratings_train_pred, ratings_test, ratings_test_pred)
Wir haben hohe Fehlerraten beim sowhl Traning als auch beim Testing, hier ist also eine klare Underfitting zu erkennen.
Einige AO-Titels wurde als E geschätz, problematisch!
print("Numbers of leaves:", clf.get_n_leaves())
print("Numbers of nodes:", clf.tree_.node_count)
tree.plot_tree(clf, class_names=class_names_ordered, filled=True)
plt.show()
Numbers of leaves: 959 Numbers of nodes: 1917
Pruning the tree¶
Diesmal nehmen wir eien Aplha-Wert von 0.006
# Limit numbers of nodes
clf = tree.DecisionTreeClassifier(random_state=0, ccp_alpha=0.006)
clf.fit(features_train, ratings_train)
DecisionTreeClassifier(ccp_alpha=0.006, random_state=0)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
DecisionTreeClassifier(ccp_alpha=0.006, random_state=0)
Numbers of leaves: 11 Numbers of nodes: 21
1911 Knoten -> 21 Knoten, 91x Reduktion!¶
Genaulichkeitverlust ist hier etwa 10%.
Class Weights¶
AO-Titel wurden als alles außer sich selbst klassifiziert. Das liegt daran, dass die AO-Titel sehr wenige Einträge haben (27 insgesamt). Wir können das Problem mit class_weight lösen, indem wir die Klassen gewichten.
clf = tree.DecisionTreeClassifier(random_state=0, ccp_alpha=0.006,
class_weight="balanced") # <-
clf.fit(features_train, ratings_train)
ratings_train_pred = clf.predict(features_train)
ratings_test_pred = clf.predict(features_test)
show_accuracy_plots(ratings_train, ratings_train_pred, ratings_test, ratings_test_pred)
Gewichtete Konfusionsmatrix und Genauigkeit¶
{'0': 0, '6': 1, '12': 2, '16': 3, '18': 4}
show_accuracy_plots(ratings_train, ratings_train_pred, ratings_test, ratings_test_pred, True)
Unser Model hat eine niedrige Genauigkeit bei den E und E10+ Titeln. Das liegt daran, dass die Unterschiede zwischen E und E10+ Titeln eher gering ist.
Fußnoten¶
Was kann man noch mit diesen Daten machen?¶
Bessere Klassifikation in Verbindung mit Spielgeneren¶
Features zusammengruppieren¶
Die Dimensionen der Features können reduziert werden, indem wir änlichen Features zusammenfassen. Z.B. "Gewalt" und "Blut". Hier können Methoden wie PCA (Principal Component Analysis) helfen.
In PCA kombinieren wir die Features zu Hauptkomponenten (Principal Components), die die Varianz der Daten maximieren. Dadurch können wir die Anzahl der Features reduzieren und gleichzeitig die Informationen erhalten.