Multi-Table Tutorial with the Scikit-learn API
In this notebook, we will learn how to train a classifier for a simple multi-table dataset. It is recommended to see the single table tutorial first.
The Accidents Dataset
We'll train a multi-table classifier on a the dataset Accidents. This dataset describes the characteristics of traffic accidents that happened in France in 2018. It has two tables with the following schema:
- The main table
Accident - The table
Vehiclein a0:nrelationship withAccident - The table
Userin a0:nrelationship withVehicle - The table
Placein a0:1relationship withAccident
Let's first check the content of the tables. The fields of each table need to be typed explicitly when read via pandas.read_csv, especially when typing confusions can be made; most often, this applies to fields which have numerical values, but are categorical in nature. For example:
- for the
Accidenttable, this concerns columnsDepartmentandCommune: their values are integers, but the nature of these columns is categorical, because these are labels; - for the
Usertable, this concerns theSeatcolumn: its values are also integers, but the nature of this column is categorical, because it identifies the seat whe user was sitting on at the time of the accident.
# Method 1: Load data directly from GitHub (recommended for quick tests or small datasets)
url_datasets = "https://raw.githubusercontent.com/KhiopsML/khiops-samples/11.0.0"
accidents_df = pd.read_csv(f"{url_datasets}/Accidents/Accidents.txt", delimiter="\t", dtype={
"Department": pd.CategoricalDtype(),
"Commune": pd.CategoricalDtype()
}
)
vehicles_df = pd.read_csv(f"{url_datasets}/Accidents/Vehicles.txt", delimiter='\t')
users_df = pd.read_csv(f"{url_datasets}/Accidents/Users.txt", delimiter='\t', dtype={
"Seat": pd.CategoricalDtype()
}
)
places_df = pd.read_csv(f"{url_datasets}/Accidents/Places.txt", delimiter='\t', low_memory=False)
# Method 2: Load data locally after downloading all Khiops samples (best for offline use)
# from khiops.tools import download_datasets
# download_datasets()
# accidents_dataset_dir = f"{kh.get_samples_dir()}/Accidents"
# accidents_df = pd.read_csv(f"{accidents_dataset_dir}/Accidents.txt", sep="\t",
# "Department": pd.CategoricalDtype(),
# "Commune": pd.CategoricalDtype()
# }
# )
# vehicles_df = pd.read_csv(f"{accidents_dataset_dir}/Vehicles.txt", sep="\t")
# users_df = pd.read_csv(f"{accidents_dataset_dir}/Users.txt", sep="\t",
# "Seat": pd.CategoricalDtype()
# }
# )
# places_df = pd.read_csv(f"{accidents_dataset_dir}/Places.txt", sep="\t", low_memory=False)
# Display the first records from each table
print("Accidents table:")
display(accidents_df.head(5))
print("Vehicles table:")
display(vehicles_df.head(5))
print("Users table:")
display(users_df.head(5))
print("Places table:")
display(places_df.head(5))
| AccidentId | Gravity | Date | Hour | Light | Department | Commune | InAgglomeration | IntersectionType | Weather | CollisionType | PostalAddress | GPSCode | Latitude | Longitude | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 201800000001 | NonLethal | 2018-01-24 | 15:05:00 | Daylight | 590 | 005 | No | Y-type | Normal | 2Vehicles-BehindVehicles-Frontal | route des Ansereuilles | M | 50.55737 | 2.55737 |
| 1 | 201800000002 | NonLethal | 2018-02-12 | 10:15:00 | Daylight | 590 | 011 | Yes | Square | VeryGood | NoCollision | Place du général de Gaul | M | 50.52936 | 2.52936 |
| 2 | 201800000003 | NonLethal | 2018-03-04 | 11:35:00 | Daylight | 590 | 477 | Yes | T-type | Normal | NoCollision | Rue nationale | M | 50.51243 | 2.51243 |
| 3 | 201800000004 | NonLethal | 2018-05-05 | 17:35:00 | Daylight | 590 | 052 | Yes | NoIntersection | VeryGood | 2Vehicles-Side | 30 rue Jules Guesde | M | 50.51974 | 2.51974 |
| 4 | 201800000005 | NonLethal | 2018-06-26 | 16:05:00 | Daylight | 590 | 477 | Yes | NoIntersection | Normal | 2Vehicles-Side | 72 rue Victor Hugo | M | 50.51607 | 2.51607 |
| AccidentId | VehicleId | Direction | Category | PassengerNumber | FixedObstacle | MobileObstacle | ImpactPoint | Maneuver | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 201800000001 | A01 | Unknown | Car<=3.5T | 0 | NaN | Vehicle | RightFront | TurnToLeft |
| 1 | 201800000001 | B01 | Unknown | Car<=3.5T | 0 | NaN | Vehicle | LeftFront | NoDirectionChange |
| 2 | 201800000002 | A01 | Unknown | Car<=3.5T | 0 | NaN | Pedestrian | NaN | NoDirectionChange |
| 3 | 201800000003 | A01 | Unknown | Motorbike>125cm3 | 0 | StationaryVehicle | Vehicle | Front | NoDirectionChange |
| 4 | 201800000003 | B01 | Unknown | Car<=3.5T | 0 | NaN | Vehicle | LeftSide | TurnToLeft |
| AccidentId | VehicleId | Seat | Category | Gender | TripReason | SafetyDevice | SafetyDeviceUsed | PedestrianLocation | PedestrianAction | PedestrianCompany | BirthYear | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 201800000001 | A01 | 1 | Driver | Male | Leisure | SeatBelt | Yes | NaN | NaN | Unknown | 1960.0 |
| 1 | 201800000001 | B01 | 1 | Driver | Male | NaN | SeatBelt | Yes | NaN | NaN | Unknown | 1928.0 |
| 2 | 201800000002 | A01 | 1 | Driver | Male | NaN | SeatBelt | Yes | NaN | NaN | Unknown | 1947.0 |
| 3 | 201800000002 | A01 | NaN | Pedestrian | Male | NaN | Helmet | NaN | OnLane<=OnSidewalk0mCrossing | Crossing | Alone | 1959.0 |
| 4 | 201800000003 | A01 | 1 | Driver | Male | Leisure | Helmet | Yes | NaN | NaN | Unknown | 1987.0 |
| AccidentId | RoadType | RoadNumber | RoadSecNumber | RoadLetter | Circulation | LaneNumber | SpecialLane | Slope | RoadMarkerId | RoadMarkerDistance | Layout | StripWidth | LaneWidth | SurfaceCondition | Infrastructure | Localization | SchoolNear | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 201800000001 | Departamental | 41 | NaN | C | TwoWay | 2.0 | 0 | Flat | NaN | NaN | RightCurve | NaN | NaN | Normal | Unknown | Lane | 0.0 |
| 1 | 201800000002 | Communal | 41 | NaN | D | TwoWay | 2.0 | 0 | Flat | NaN | NaN | LeftCurve | NaN | NaN | Normal | Unknown | Lane | 0.0 |
| 2 | 201800000003 | Departamental | 39 | NaN | D | TwoWay | 2.0 | 0 | Flat | NaN | NaN | Straight | NaN | NaN | Normal | Unknown | Lane | 0.0 |
| 3 | 201800000004 | Departamental | 39 | NaN | NaN | TwoWay | 2.0 | 0 | Flat | NaN | NaN | Straight | NaN | NaN | Normal | Unknown | Lane | 0.0 |
| 4 | 201800000005 | Communal | NaN | NaN | NaN | OneWay | 1.0 | 0 | Flat | NaN | NaN | Straight | NaN | NaN | Normal | Unknown | Lane | 0.0 |
Training the Classifier
We start by creating our X and y for the fit method. For multi-table tasks KhiopsClassifier uses a multi-table dataset specification: It is a dictionary that describes the schema of the dataset:
X = {
"main_table": (accidents_df.drop("Gravity", axis=1), ["AccidentId"]), # We drop the target column "Gravity"
"additional_data_tables": {
"Vehicles": (vehicles_df, ["AccidentId", "VehicleId"]),
"Vehicles/Users": (users_df, ["AccidentId", "VehicleId"]),
"Places": (places_df, ["AccidentId"], True),
},
}
y = accidents_df["Gravity"]
Note the main table has one key (AccidentId) and the secondary table Vehicles has two (AccidentId and VehicleId).
To describe relations between tables, the data paths are used as keys in the dictionary of table specifications. A data path is a string of "/"-separated table variable names, except for the main table name (which is the implicit first element in the data path)
The khiops library provides the helper function train_test_split_dataset that splits a multi-table specification into two specs for train and test; by default, 25 % of the data are included in the test dataset:
We now fit our classifier on the train split. By default, the Khiops creates at most 100 multi-table variables (n_features) and 10 random decision trees (n_trees). We change these values for this example:
KhiopsClassifier(n_features=1000, n_trees=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.
KhiopsClassifier(n_features=1000, n_trees=0)
Displaying the Classifiers’ Training Accuracy and AUC
The fit method calculates evaluation metrics on the training dataset. We access them via the estimator's attribute model_report_ which is an instance of the AnalysisResults class. Let's check this out:
Accidents train accuracy: 0.94402
Accidents train auc : 0.849142
The PredictorPerformance object has also a confusion matrix attribute:
| Lethal | NonLethal | |
|---|---|---|
| Lethal | 80 | 71 |
| NonLethal | 2355 | 40831 |
If you have installed the Khiops Visualization app you may explore the full learning report by executing the code below.
Deploying the Classifier and Displaying Its Test Performance
Now that we have a fitted KhiopsClassifier, we are now going to deploy it on the test split.
This can be done in two different ways:
- to predict a class that can be obtained using its
predict. - to predict class probabilities that can be obtained using its
predict_proba.
Let's first predict the Accident labels:
Classes:
array(['Lethal', 'NonLethal'], dtype='<U9')
Predictions (first 10 values):
array(['NonLethal', 'NonLethal', 'NonLethal', 'NonLethal', 'NonLethal',
'NonLethal', 'NonLethal', 'NonLethal', 'NonLethal', 'NonLethal'],
dtype=object)
Probabilities (first 10 rows):
array([[3.33350375e-02, 9.66664963e-01],
[1.47499584e-01, 8.52500416e-01],
[2.74770163e-03, 9.97252298e-01],
[6.58668424e-04, 9.99341332e-01],
[5.30796355e-02, 9.46920364e-01],
[4.05598915e-03, 9.95944011e-01],
[1.16307100e-01, 8.83692900e-01],
[3.84458375e-03, 9.96155416e-01],
[3.99484426e-03, 9.96005156e-01],
[5.09931321e-02, 9.49006868e-01]])
From these predictions we compute the test accuracy and AUC (One-vs-Rest) scores using sklearn.metrics
Accidents test accuracy: 0.9474595043610688
Accidents test auc : 0.8313873336221049