Séance 5 :

Amélioration du filtre médian, définition des Staypoints, application des KMeans, DBSCAN clustering des segments

1 Mars

Objectifs de la séance

Nous nous sommes fixés pour objectifs de :

  • Identifier au sein d'un jeu entier de données, les habitudes de déplacement en fonction de l'heure de la journée.
  • Améliorer la robustesse de l'algorithme de classification des vitesses.
  • Corriger la représentaion des segments vitesse.
  • Tester un nouvel algorithme de clustering sur les segments de trajets déduit précédemment.

Remarques

Chaque partie de ce notebook est indépendante. Une fois que I. Fonctions préliminaires a été exécuté, il est donc possible de ne recompiler qu'une partie.

I. Fonctions préliminaires

A. Imports

In [5]:
import importlib
import gmplot
import parser
import filters
import distance
import colors
import staypoint as st
from projectColors import defineColorsList,defineColorsShortList
import datetime
import speedClassification as speedClass
from tqdm import tqdm
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from sklearn.cluster import DBSCAN
from IPython.display import IFrame
In [2]:
lColors=defineColorsList()
colorListSpeed=defineColorsShortList()

II.Calendrier des déplacements

L'idée générale est de visualiser sur l'ensemble des données quand est-ce que l'utilisateur est généralement en mouvement. Ainsi on pourra par la suite visualier si il y a une différence entre jour de la semaine/week-end.

A. Préparations des données

On commence par prendre l'ensemble des données de l'utilisateur Android. Ces données s'étalent du 5/04/2015 au 01/02/2018.

In [3]:
android_df = parser.importJson("data/android.json", True)

On applique dans un premier temps un mean-filter sur l'ensemble des données avec une fenêtre de 10 points, puis l'algorithme stay-point afin de pouvoir identifiier les phases en mouvement :

In [4]:
df_all_year = filters.meanFilter(android_df, 10)
stay_point_all = st.findStayPoints(df_all_year, 3, 50, 5)
In [6]:
gmap = gmplot.GoogleMapPlotter(45.764376, 4.810495, 13, apikey="AIzaSyDsYwvF3UUxTx8RB40wd4SnUVzfnbW66LM")
segment_count = max(stay_point_all["segment_mouvement"])

for l in range(segment_count):
    segment = stay_point_all[stay_point_all['segment_mouvement'] == l]
    segment_mouvement = segment[segment['is_mouvement'] == True ]
    gmap.plot(segment_mouvement["lat_mean_filt"], segment_mouvement["lng_mean_filt"], lColors[l%20], edge_width=4)

gmap.draw("figure/5-segmented-year-df.html")
In [7]:
IFrame('figure/5-segmented-year-df.html', width=990, height=500)
Out[7]:

On ne garde ensuite que les points pour lesquels l'utilisateur était en mouvement :

In [8]:
mouvement_all = stay_point_all[stay_point_all['is_mouvement'] == True]
mouvement_all.head()
Out[8]:
timestampMs latitude longitude date time delay distance velocity acceleration lat_mean_filt lng_mean_filt is_mouvement segment_mouvement
9 1517468697240 45.780492 4.775161 01-02-2018 08:04:57 66.436 0.000000 0.000000 0.000000 45.780492 4.775161 True 2
10 1517468637169 45.780492 4.775161 01-02-2018 08:03:57 60.071 0.000000 0.000000 0.000000 45.779441 4.778987 True 2
11 1517468572078 45.780492 4.775161 01-02-2018 08:02:52 65.091 0.000000 0.000000 0.000000 45.777878 4.781666 True 2
12 1517468511977 45.780492 4.775161 01-02-2018 08:01:51 60.101 851.290427 50.994135 3.054659 45.776303 4.784295 True 2
13 1517468451879 45.785586 4.766965 01-02-2018 08:00:51 60.098 1410.906743 83.963108 4.996647 45.774730 4.786968 True 2

B. Regroupement des données selon les heures de la journée

On regroupe ensuite les points par minutes.

La colonne is_mouvement présente alors le nombre de fois où il y a un point en mouvement à cette heure.

In [9]:
mouvement_all['time'] = mouvement_all['time'].transform(lambda x: x.str[0:5])

grouped_mouvement_all = mouvement_all.groupby('time', 
            as_index=False)[['is_mouvement']].sum()
grouped_mouvement_all.head()

times = []
for hours in grouped_mouvement_all['time']:
    times.append(datetime.time(int(hours[0:2]),
                              int(hours[3:5])))
grouped_mouvement_all['time'] = times
grouped_mouvement_all.head()
Out[9]:
time is_mouvement
0 00:00:00 76.0
1 00:01:00 51.0
2 00:02:00 56.0
3 00:03:00 46.0
4 00:04:00 58.0

On représente ensuite le nombre de points en fonction de l'heure :

  • On peut voir que l'utilisateur est très souvent en mouvement le matin vers 9h, vers 12h, et vers 18h
  • Il est très peu souvent en mouvement entre 2h et 5h du matin
In [10]:
plt.figure(figsize=(12,8))
plt.grid(True)
plt.plot(grouped_mouvement_all.time
         ,grouped_mouvement_all.is_mouvement)
plt.locator_params('x', nbins=15)
plt.xlabel('Time of the day')
plt.ylabel('Number of mouvement recordings')
plt.show()

Nous avons donc fait une première analyse "à la main", l'idée serait donc de la généraliser grâce à un algorithme de clusterisation. On reprendre le même KMeans que l'on avait utilisé pour segmenter en fonction des régimes de vitesse et on l'adapte au cas présent. On cherche à identfier 3 cas possibles :

  • peu en mouvement
  • parfois en mouvement
  • souvent en mouvement

B. Application de l'algorithme Kmeans

In [11]:
import mouvementClassification as mouvementClass

On découpe la journée en 48 segments, correspondant à des périodes d'une demi-heure.

In [12]:
(lK,whitened)=mouvementClass.applyKMeans(grouped_mouvement_all,k=48)
lBoundiaries=mouvementClass.getBoundiaries(lK)
lFirstSpeedSegmentation=mouvementClass.calcFirstSegmentation(lBoundiaries,whitened,bPadd=True)
In [13]:
plt.figure(figsize=(12,8))
plt.grid(True)
plt.ylim(0,7)
for ii, plots in enumerate(lFirstSpeedSegmentation):
    plt.plot(plots,lColors[ii%20])
plt.xlabel('Time of the day')
plt.ylabel('Number of mouvement recordings')
            
plt.show()

Nous pouvons visualiser sur la figure ci-dessus le partage du signal en 48 segments homogènes. Nous souhaitons maintenant classifier ces différents segments dans les 3 classes que nous avons défini précédemment.

In [14]:
(l,a)=mouvementClass.agglomerateSpeedSegments(lFirstSpeedSegmentation)
In [15]:
group_mouv = []
segment_num = []
offset = 0;
colors = []
m = 0;
for ii, plots in enumerate(l):
            for jj, speed in enumerate(plots):
                if jj >= offset :
                    group_mouv.append(a[ii])
                    segment_num.append(m)
                new_offset = len(plots)      
            offset = new_offset
            m += 1
            colors.append(colorListSpeed[a[ii]])
group_mouv.append(a[ii])
segment_num.append(m)
In [16]:
grouped_mouvement_all['group_mouv'] = group_mouv
grouped_mouvement_all['segment_num'] = segment_num
In [17]:
plt.figure(figsize=(12,8))
plt.grid(True)
segment_count = max(grouped_mouvement_all["segment_num"])
for m in range(segment_count):
    segment = grouped_mouvement_all[grouped_mouvement_all['segment_num'] == m]
    plt.plot(segment.time, segment.is_mouvement, color=colors[m])
plt.xlabel('Time of the day')
plt.ylabel('Number of mouvement recordings')
plt.show()
  • En vert l'utilisateur est peu en mouvement
  • En orange il est parfois en mouvement
  • En rouge il est souvent en mouvement.

On retrouve donc ce que l'on avait dit plus haut : Entre 2h et 6h du matin l'utilisateur est donc très peu souvent en mouvement (il dort!).

Vers 9h, autour de 12h, à 16h et autour de 18h et en début de soirée il est très souvent en mouvement.

C. Données Iphone

Nous allons ici réitérer notre démarche avec les données de l'utilisateur iPhone, qui possède bien moins de points. Cela nous permet d'évaluer la capacité de généralisation de notre méthode.

In [18]:
iphone_df = parser.importJson("data/iphone.json", True)
In [19]:
dfi_all_year = filters.meanFilter(iphone_df, 10)
stayi_point_all = st.findStayPoints(dfi_all_year, 3, 50, 5)
In [20]:
mouvementi_all = stayi_point_all[stayi_point_all['is_mouvement'] == True]
In [21]:
mouvementi_all['time'] = mouvementi_all['time'].transform(lambda x: x.str[0:5])

groupedi_mouvement_all = mouvementi_all.groupby('time', 
            as_index=False)[['is_mouvement']].sum()
groupedi_mouvement_all.head()

times = []
for hours in groupedi_mouvement_all['time']:
    times.append(datetime.time(int(hours[0:2]),
                              int(hours[3:5])))
groupedi_mouvement_all['time'] = times
In [22]:
plt.figure(figsize=(12,8))
plt.grid(True)
plt.plot(groupedi_mouvement_all.time
         ,groupedi_mouvement_all.is_mouvement)
plt.locator_params('x', nbins=15)
plt.xlabel('Time of the day')
plt.ylabel('Number of mouvement recordings')
plt.show()
In [23]:
(lK,whitened)=mouvementClass.applyKMeans(groupedi_mouvement_all,k=48)
lBoundiaries=mouvementClass.getBoundiaries(lK)
lFirstSpeedSegmentation=mouvementClass.calcFirstSegmentation(lBoundiaries,whitened,bPadd=True)
In [28]:
(l,a)=mouvementClass.agglomerateSpeedSegments(lFirstSpeedSegmentation)
In [29]:
group_mouv = []
segment_num = []
offset = 0;
colors = []
m = 0;
for ii, plots in enumerate(l):
            for jj, speed in enumerate(plots):
                if jj >= offset :
                    group_mouv.append(a[ii])
                    segment_num.append(m)
                new_offset = len(plots)      
            offset = new_offset
            m += 1
            colors.append(colorListSpeed[a[ii]])
group_mouv.append(a[ii])
segment_num.append(m)
In [30]:
groupedi_mouvement_all['group_mouv'] = group_mouv
groupedi_mouvement_all['segment_num'] = segment_num
In [32]:
plt.figure(figsize=(12,8))
plt.grid(True)
segment_count = max(groupedi_mouvement_all["segment_num"])
for m in range(segment_count):
    segment = groupedi_mouvement_all[groupedi_mouvement_all['segment_num'] == m]
    plt.plot(segment.time, segment.is_mouvement, color=colors[m])
plt.xlabel('Time of the day')
plt.ylabel('Number of mouvement recordings')
plt.show()

L'utilisateur iphone semble plus dormir la nuit !

Nous arrivons à un résultat très satisfaisant, l'idée serait ensuite de regarder suivant jours de la semaine/week-end voir si l'on observe une différence.

III. DBSCAN clustering sur les segments

Nous nous proposons ici d'appliquer l'algorithme DBSCAN pour agglomérer les différents trajets au cours d'une journée.

La segmentation de la journée en trajets sera réalisé en utilisant l'approche présentée dans le notebook 3.

A. Principes de DBSCAN et préparation des données

DBSCAN (density-based spatial clustering of applications with noise) est un algorithme de partitionnement de données basé sur la densité des clusters pour effectuer le partitionnement.

Deux paramètres principaux à prendre en compte par DBSCAN:

  • eps : la distance maximale entre deux observations leur permettant d'appartenir au même voisinage,
  • min_samples : le nombre minimal de points appartennant au voisinage d'un point pour qu'il soit considéré comme un "core point"

En effet, on a 3 types de points:

  • " core point " : tout point ayantau moins le nombre min_samples de points dans son voisinage,
  • " border point " : tout point qui n'est pas un "core point" mais qui a un "core point" dans son voisinage,
  • " noise point " : tout point qui n'est ni "core point" ni "border point" qui sera considéré comme un bruit (outlier).

B. Préparation des données

On se propose de tester DBSCAN sur les données de la journée "17-08-2017" sur laquelle on a travaillé précédemment.

In [24]:
day_df = parser.selectDate("17-08-2017", android_df)

Nous appliquons la fonction de segmentation de trajets vue au cours de la séance "3-segmentation" pour pouvoir ensuite effectuer un partitionnement sur les segments obtenus.

Partage en segments¶

In [25]:
def delay_segment_dataframe(df, limit) :
    segnum = 0
    segments = []

    for i in range(df["time"].size) :
        if (df["delay"][i] > limit) :
            segments.append(segnum)
            segnum += 1;
        else :
            segments.append(segnum)

    df["segment"] = segments
    return df
In [26]:
day_df = delay_segment_dataframe(day_df, limit=100)
In [27]:
segment_count = max(day_df['segment'])
In [28]:
gmap = gmplot.GoogleMapPlotter(45.757589, 4.831689, 14, apikey="AIzaSyDsYwvF3UUxTx8RB40wd4SnUVzfnbW66LM")

for i in range(segment_count) :
    start_index = day_df[day_df['segment'] == i].index.tolist()[0]
    end_index = day_df[day_df['segment'] == i + 1].index.tolist()[0]

    segment_df = day_df.loc[start_index:(end_index - 1),]
    gmap.plot(segment_df["latitude"], segment_df["longitude"], lColors[i%20], edge_width=4)

gmap.draw("figure/5-dbscan.html")
IFrame('figure/5-dbscan.html', width=990, height=500)
Out[28]:

Préparation des données pour le DBSCAN¶

Pour la préparation des données, nous prenons les segments comme les observations qui seront répartis suivant notre algorithme.

In [29]:
day_df_grp_seg=day_df.groupby(['segment']).mean()

Un segment sera donc caractérisé par la moyenne de :

  • "latitude" et "longitude" qui carectérisent le lieu moyen trajet, permettant de le locaiser géographiquement d'une manière approximative,
  • "delay" caractérisant la moyenne de l'espacement temporel entre les obeservations
  • "distance" caractérisant la disance moyenne parcourue entre les observations consécutives
  • "velocity" et "acceleration" nous renseignent sur le moyen de transport utilisé lors du ségment de trajet en question.

C. Application de l'algorithme DBSCAN

Comme première exploration de l'algorithme DBSCAN, nous englobons toutes les variables considérées dans le dataframe vu précédamment pour obtenir un partitionnement selon la position géographique, la prise des valeurs dans le temps (retards), la distance séparant deux observations consécutives, la vitesse et l'accélération moyenne.

In [30]:
db = DBSCAN(eps=12, min_samples=3).fit(day_df_grp_seg)
clusters=db.fit_predict(day_df_grp_seg)
In [31]:
plt.figure(figsize=(12,8))
plt.plot(clusters,'-o')
plt.grid(True)
plt.ylabel('Cluster number')
plt.xlabel('Segment Num')
Out[31]:
Text(0.5,0,'Segment Num')

On a repéré 3 clusters: le cluster 0, 1 et 2.

Les segments ayant une valeur -1 sont considérés comme outliers par l'agorithme DBSCAN, et sont donc excluts des autres clusters.

Chaque segment sera donc attribué à un cluster ou considéré comme un bruit (outlier) comme suit:

In [32]:
day_df_grp_seg["clusters"]=clusters

Nous donnons un aperçu sur la moyenne de chaque cluster formé:

In [33]:
day_df_grp_clust=day_df_grp_seg.groupby(['clusters']).mean()
day_df_grp_clust
Out[33]:
latitude longitude delay distance velocity acceleration lat_mean_filt lng_mean_filt is_mouvement segment_mouvement
clusters
-1 45.759378 4.836712 460.862987 33.864962 4.893899 0.877182 45.759421 4.836657 0.049853 4324.722914

Il est à noter que, contrairement à l'algorithme Kmeans, DBSCAN repose dans sa segmentation sur la densité et ne fait pas intervenir la notion de centroides des clusters formés. D'ou la possibilité d'avoir un point moyen d'un cluster qui n'y appartient pas, mais appartenant à un autre cluster.

Pour une représentation graphique, il est judicieux de modifier le dataframe initial en y ajoutant, de plus de la colonne segment, la colonne clusters indiquant le cluster auquel appartient le segment incluant chaque observation.

In [34]:
day_df['clusters']=day_df_grp_seg.iloc[day_df.segment]['clusters'].tolist()
day_df.head()
Out[34]:
timestampMs latitude longitude date time delay distance velocity acceleration lat_mean_filt lng_mean_filt is_mouvement segment_mouvement segment clusters
0 1503007181042 45.765661 4.835965 17-08-2017 23:59:41 21.280 2.060989 0.357414 0.061982 45.765643 4.835956 False 4239 0 -1
1 1503007160283 45.765642 4.835962 17-08-2017 23:59:20 20.759 2.060989 0.356864 0.061792 45.765642 4.835955 False 4239 0 -1
2 1503007139492 45.765661 4.835965 17-08-2017 23:58:59 20.791 0.000000 0.000000 0.000000 45.765642 4.835955 False 4239 0 -1
3 1503007118676 45.765661 4.835965 17-08-2017 23:58:38 20.816 2.060989 0.359301 0.062638 45.765643 4.835955 False 4239 0 -1
4 1503007098026 45.765642 4.835962 17-08-2017 23:58:18 20.650 0.000000 0.000000 0.000000 45.765649 4.835962 False 4239 0 -1

D. Représentation des clusters sur google map

Nous représentons sur la carte les différents segments de trajets chacun colorié selon le cluster auquel il appartient. Les segments détectés comme outliers seront également représentés par une même couleur, dans notre cas le noir.

In [35]:
gmap = gmplot.GoogleMapPlotter(45.757589, 4.831689, 14, apikey="AIzaSyDsYwvF3UUxTx8RB40wd4SnUVzfnbW66LM")

for i in range(segment_count) :
    start_index = day_df[day_df['segment'] == i].index.tolist()[0]
    end_index = day_df[day_df['segment'] == i + 1].index.tolist()[0]

    segment_df = day_df.loc[start_index:(end_index - 1),]
    gmap.plot(segment_df["latitude"], segment_df["longitude"], lColors[i%20], edge_width=4)

gmap.draw('figure/5-segment-clustered-dbscan.html')
IFrame('figure/5-segment-clustered-dbscan.html', width=990, height=500)
Out[35]:

Nous remarquons à première vue que le partitionnement des segments est fortement influencé par la localisation géographique.

Cela peut être expliqué par l'utilisation habituelle par l'individu d'un même moyen de transport pour se déplacer dans une même zone géographique.

Nous pouvons, dans une prochaine étape, attribuer un poids plus importants aux variables "velocity" et "acceleration". Celles-ci devraient caractériser plus clairement les moyens de transport utilisés sur un trajet.

IV. Amélioration de la classification des vitesses

La classification des vitesses réalisée dans la séance précédente comportait certaines faiblesses :

  • présence de bruit haut fréquence dans la classification
  • problème de raccord lors de la représentation visuel de la classification

Dans cette partie, nous essayerons de traiter ces deux problèmes.

A. Chargement des données

In [36]:
day_df2 = parser.selectDate("25-11-2017", android_df)
day_df2 = filters.meanFilter(day_df2, 10)
In [37]:
stay_point_df2 = st.findStayPoints(day_df2,3,20,5)
In [38]:
day_df2['distance'] = distance.getDistances(day_df2)
day_df2['velocity'] = distance.getVelocities(day_df2)
day_df2['speedClass'] = speedClass.initSpeedClass(day_df2)
stay_point_df2['numSC'] = speedClass.initSpeedClass(stay_point_df2)

B. Classification des vitesses

La classification des vitesses présente des variations hautes-fréquences peu vraisemblables, comme nous pouvons le voir sur la figure suivante.

In [39]:
segment_count = max(stay_point_df2["segment_mouvement"])
for iSeg in range(segment_count):
    segment = stay_point_df2[stay_point_df2['segment_mouvement'] == iSeg]
    segment_mouvement = segment[segment['is_mouvement'] == True ]
    if len(segment_mouvement['velocity'])>5:
        (lK,whitened)=speedClass.applyKMeans(segment_mouvement,
                                             k=5)
        lBoundiaries=speedClass.getBoundiaries(lK)
        lFirstSpeedSegmentation=speedClass.calcFirstSegmentation(lBoundiaries,whitened,bPadd=False)
        (speedAgglomerates,a)=speedClass.agglomerateSpeedSegments(lFirstSpeedSegmentation,
                                                                  lowThreshold=0.4,
                                                                  highThreshold=1.4,bMedian=False)            
        offset=0
        for ii, plots in enumerate(speedAgglomerates):
            for jj, speed in enumerate(plots):
                stay_point_df2['speedClass'][segment_mouvement.index.tolist()[jj+offset]]=a[ii]
                stay_point_df2['numSC'][segment_mouvement.index.tolist()[jj+offset]]=ii
            offset+=jj
/usr/local/lib/python3.6/site-packages/numpy/core/fromnumeric.py:2957: RuntimeWarning: Mean of empty slice.
  out=out, **kwargs)
/usr/local/lib/python3.6/site-packages/numpy/core/_methods.py:80: RuntimeWarning: invalid value encountered in double_scalars
  ret = ret.dtype.type(ret / rcount)
In [40]:
plt.figure(figsize=(12,8))
plt.subplot(211)
plt.plot( stay_point_df2['speedClass'],linewidth=2)
plt.grid(True)
plt.ylim(0,3)
plt.ylabel('Regime vitesse')
plt.xlabel('Numero d\'échantillons')
plt.subplot(212)
plt.ylim(0,150)
plt.grid(True)
plt.xlabel('Numero d\'échantillons')
plt.ylabel('Vitesse (km/h)')
segment_count = max(stay_point_df2["segment_mouvement"])
for l in range(segment_count):
    segment = stay_point_df2[stay_point_df2['segment_mouvement'] == l]
    segment_mouvement = segment[segment['is_mouvement'] == True ]
    plt.plot(segment_mouvement['velocity'], color=lColors[l%20])
plt.show()

Les hautes fréquences sont notamment visibles lors de la classification du second trajet, entre 100 et 150.

Ces hautes fréquences sont causées par une sous-segmentation trop importante. En effet, jusqu'à présent, nous avons considéré qu'un trajet pouvait être sous-segmenté en 5 régimes vitesses; quelle que soit la durée du trajet.

Pour résoudre ce problème, nous avons indexé le nombre de régimes vitesses sur la longueur du trajet. Nous obtenons dans cette configuration la figure suivante.

In [41]:
segment_count = max(stay_point_df2["segment_mouvement"])
for iSeg in range(segment_count):
    segment = stay_point_df2[stay_point_df2['segment_mouvement'] == iSeg]
    segment_mouvement = segment[segment['is_mouvement'] == True ]
    if len(segment_mouvement['velocity'])>5:
#         print(int(len(segment_mouvement['velocity'])/20)+1)
        (lK,whitened)=speedClass.applyKMeans(segment_mouvement,
                                             k=int(len(segment_mouvement['velocity'])/30)+1)
        lBoundiaries=speedClass.getBoundiaries(lK)
        lFirstSpeedSegmentation=speedClass.calcFirstSegmentation(lBoundiaries,whitened,bPadd=False)
        (speedAgglomerates,a)=speedClass.agglomerateSpeedSegments(lFirstSpeedSegmentation,
                                                                  lowThreshold=0.4,
                                                                  highThreshold=1.4,bMedian=False)            
        offset=0
        for ii, plots in enumerate(speedAgglomerates):
            for jj, speed in enumerate(plots):
                stay_point_df2['speedClass'][segment_mouvement.index.tolist()[jj+offset]]=a[ii]
                stay_point_df2['numSC'][segment_mouvement.index.tolist()[jj+offset]]=ii
            offset+=jj
In [42]:
plt.figure(figsize=(12,8))
plt.subplot(211)
plt.plot( stay_point_df2['speedClass'],linewidth=2)
plt.grid(True)
plt.ylim(0,3)
plt.ylabel('Regime vitesse')
plt.xlabel('Numero d\'échantillons')
plt.subplot(212)
plt.ylim(0,150)
plt.grid(True)
plt.xlabel('Numero d\'échantillons')
plt.ylabel('Vitesse (km/h)')
segment_count = max(stay_point_df2["segment_mouvement"])
for l in range(segment_count):
    segment = stay_point_df2[stay_point_df2['segment_mouvement'] == l]
    segment_mouvement = segment[segment['is_mouvement'] == True ]
    plt.plot(segment_mouvement['velocity'], color=lColors[l%20])
plt.show()

Nous constatons la disparition des hautes fréquences dans la classification des hautes vitesses.

Toutefois, la classification des vitesses est encore imparfaite. On constate en effet des erreurs de classification. Dans le 6ème segment, entre 400 et 480, on s'attend à ce que le premier régime vitesse soit classifié comment vitesse moyenne, plutot que comme vitesse lente. De même, on aurait préféré que le 3ème segment et le 4ème segment soit classifié comme vitesse lente.

Ces erreurs peuvent être dû à plusieurs élements :

  • mauvais choix des seuils
  • mauvais résumé des sous-segments (pour l'instant nous utilisons une simple moyenne)

C. Raccordement des différents régimes vitesses

Lorsque nous visualisons le résultat de la classification sur la carte, nous constatons la présence d'effets de bords : au sein d'un même trajet, les différents régimes vitesses ne sont pas raccordés.

In [45]:
gmap = gmplot.GoogleMapPlotter(45.790607, 4.835850, 12, apikey="AIzaSyDsYwvF3UUxTx8RB40wd4SnUVzfnbW66LM")
segment_count = max(stay_point_df2["segment_mouvement"])

for l in range(segment_count):
    segment = stay_point_df2[stay_point_df2['segment_mouvement'] == l]
    segment_mouvement = segment[segment['is_mouvement'] == True ]
    speedSegment=segment_mouvement[segment_mouvement.speedClass==0]
    gmap.plot(segment_mouvement["latitude"], segment_mouvement["longitude"], 'navy', edge_width=6) 
    for ispeed in range(3):
        speedSegment=segment_mouvement[segment_mouvement.speedClass==ispeed]
        for ii in range (5):
            minidf=speedSegment[speedSegment.numSC==ii]
            gmap.plot(minidf["latitude"], minidf["longitude"], colorListSpeed[ispeed], edge_width=4)

gmap.draw("figure/5-segmented-day-not-bound.html")
IFrame('figure/5-segmented-day-not-bound.html', width=990, height=500)
Out[45]:

Remarquons tout d'abord qu'ici nous travaillons sur la représentation des données sur la carte. On ne modifie donc pas les données calculées.

La solution que nous proposons est de raccorder tous les segments en utilisant une vitesse moyenne. En effet, ce raccordement apparait toujours acceptable.

Plus précisément :

  • si la transition est entre le régime 0 et 1 ou entre 1 et 2, le raccordement est licite
  • si la transition est entre le régime 0 et 2, il est acceptable de considérer qu'entre le segment lent et rapide, il y a un segment de vitesse intermédiaire

Nous obtenons alors la carte ci-dessous.

In [46]:
gmap = gmplot.GoogleMapPlotter(45.790607, 4.835850, 12, apikey="AIzaSyDsYwvF3UUxTx8RB40wd4SnUVzfnbW66LM")
segment_count = max(stay_point_df2["segment_mouvement"])

for l in range(segment_count):
    segment = stay_point_df2[stay_point_df2['segment_mouvement'] == l]
    segment_mouvement = segment[segment['is_mouvement'] == True ]
    speedSegment=segment_mouvement[segment_mouvement.speedClass==0]
    gmap.plot(segment_mouvement["latitude"], segment_mouvement["longitude"], 'orange', edge_width=4) 
    for ispeed in range(3):
        speedSegment=segment_mouvement[segment_mouvement.speedClass==ispeed]
        for ii in range (5):
            minidf=speedSegment[speedSegment.numSC==ii]
            gmap.plot(minidf["latitude"], minidf["longitude"], colorListSpeed[ispeed], edge_width=4)

gmap.draw("figure/segmented-day-bound.html")
IFrame('figure/segmented-day-bound.html', width=990, height=500)
Out[46]:

V. Comparaison des filtres

Pour comparer l'efficacité de différents filtres, nous avons généré la vérité terrain d'un trajet de bus entre la gare de Vaise et Centrale Lyon.

Partant d'un trajet mesuré avec un téléphone, nous avons projeté chaque point sur la trajectoire de bus réelle; obtenue en croisant les itinéraires TCL avec une carte GPS. Chaque point mesuré avec le GPS a donc son équivalent "réel".

Il est donc possible de calculer la distance entre la trajectoire mesurée et la trajectoire réelle.

A. Chargement des données

Données brutes du trajet¶

In [47]:
df_brut = parser.selectDate("13-10-2017", android_df)[2534:2580].reset_index(drop=True)

Données réelles¶

In [48]:
lat1=[45.780032, 45.780540, 45.780681, 45.782027, 45.783036, 45.784756, 45.785952, 45.786176, 45.787327, 45.788284, 45.788971, 45.789045, 45.789545, 45.789784, 45.789858, 45.789858, 45.789887, 45.789887, 45.790589, 45.790831, 45.790774, 45.790638, 45.790323, 45.789866, 45.789522, 45.789042, 45.787687, 45.786796, 45.786653, 45.786443, 45.787078, 45.786770, 45.787570, 45.787629, 45.788376, 45.788674, 45.788374,45.788374,45.788374,45.788374, 45.788164, 45.787282, 45.786540, 45.785641, 45.784701]#, 45.783593, 45.782866]
lng1=[4.803670, 4.804217, 4.804367, 4.805569, 4.806556, 4.804239, 4.802115, 4.801235, 4.801707, 4.801535, 4.800119, 4.799915, 4.798821, 4.798177, 4.797683, 4.797683, 4.792705, 4.791321,4.791321, 4.790811, 4.790398, 4.790248, 4.790028, 4.789621, 4.789272, 4.788768, 4.787513, 4.785560, 4.784112, 4.782910, 4.777288, 4.772578, 4.769735, 4.768785, 4.765674, 4.764708, 4.764762, 4.764762, 4.764762, 4.764762, 4.764816, 4.764847, 4.764826, 4.765159, 4.765465]#, 4.765851, 4.766130]
lat1.insert(30, 45.787075)
lng1.insert(30, 4.780214)
latR = lat1[::-1]
lngR = lng1[::-1]

df_reel=pd.DataFrame(data={'latitude':latR, 'longitude':lngR})

B. Comparaison du trajet réel et du trajet enregistré

In [49]:
def compare(df1, df2):
    error=0
    for i in range(len(df1)):
        error+=distance.haversineDistance(df1.longitude[i], df1.latitude[i], df2.longitude[i], df2.latitude[i])/len(df1)
    return(error)
In [50]:
error=compare(df_brut, df_reel)
print("Ecart moyen du trajet brut au trajet réel : "+str(error)[0:5] + " mètres.")
Ecart moyen du trajet brut au trajet réel : 145.0 mètres.
In [51]:
gmap = gmplot.GoogleMapPlotter(45.764376, 4.810495, 13, apikey="AIzaSyDsYwvF3UUxTx8RB40wd4SnUVzfnbW66LM")
gmap.plot(df_brut['latitude'],df_brut['longitude'], 'cornflowerblue', edge_width=4)
gmap.plot(df_reel['latitude'],df_reel['longitude'], 'red', edge_width=4)
gmap.draw("figure/5-ground-truth.html")
IFrame('figure/5-ground-truth.html', width=990, height=500)
Out[51]:

C. Mise en évidence du meilleur filtrage

L'objectif est ici d'utiliser la trajectoire réelle pour déterminer les meilleurs paramètres pour le filtrage de la trajectoire.

Le filtre qui présentera la trajectoire la plus proche de la trajectoire réelle sera naturellement le meilleur filtre.

In [52]:
fenetre=[]
median=[]
mean=[]
for i in range(10):
    median.append(compare(filters.medianFilter(df_brut, i, True), df_reel))
    mean.append(compare(filters.meanFilter(df_brut, i, True), df_reel))
    fenetre.append(2*i+1)
reel=[compare(df_brut, df_reel)]*10

plt.figure(figsize=(12,8))
plt.plot(fenetre, median, 'r', label='median filters')
plt.plot(fenetre, mean, 'b', label='mean filters')
plt.plot(fenetre, reel, '--g', label='données brutes')
plt.legend(loc='upper right')
plt.ylabel('Ecart moyen à la trajectoire réelle')
plt.xlabel('Taille de la fenêtre')
plt.xlim([1,19])
plt.grid(True)
plt.show()

En comparant les écarts à la trajectoire réelle on constate que le meilleur score est obtenu pour un median filter de fenêtre de taille 7 (donc de paramètre 3). Au delà de 9 on commence à dégrader la qualité de la trajectoire qui devient moins précise que les données brutes.

On constate également que pour de petites fenêtres le mean filter est légèrement meilleur, mais sa performance décroit plus vite que celle du median filter.

III. Conclusion

A. Bilan

Cette séance de travail nous a apporté un certain nombre de résultats intéressants.

Nous avons montré qu'il est possible d'analyser rapidement les habitudes de déplacement d'une personne. La modélisation actuelle est un début intéressant; mais nous souhaiterions améliorer encore la visualisation des données.

Le travail sur l'agglomération des segments utilisant le DBSCAN débute, mais il présente une alternative intéresssante.

La classification des vitesses atteint des performances satisfaisantes.

La construction d'une vérité terrain nous a permi de conclure quant aux meilleurs filt

B. Travail à faire de la prochaine séance

Nous nous fixons pour objectifs de :

  • Regarder les différences jours de la semaine/week-end
  • Commencer à travailler sur pipeline complet