<jupyter_start><jupyter_text>2A.ml - Clustering - correctionCe notebook utilise les données des vélos de Chicago [Divvy Data](https://www.divvybikes.com/system-data). Il s'inspire du challenge créée pour découvrir les habitudes des habitantes de la ville [City Bike](http://www.xavierdupre.fr/app/ensae_projects/helpsphinx/challenges/city_bike.html). L'idée est d'explorer plusieurs algorithmes de clustering et comment trafiquer les données pour les faire marcher.<jupyter_code>from jyquickhelper import add_notebook_menu
add_notebook_menu()
%matplotlib inline<jupyter_output><empty_output><jupyter_text>Les donnéesElles ont été prétraitées selon le notebook [Bike Pattern 2](http://www.xavierdupre.fr/app/ensae_projects/helpsphinx/notebooks/city_bike_solution_cluster_start.html). Elles représentent la distribution du nombre de vélos partant (*startdist*) et arrivant (*stopdist*). On utilise le clustering pour découvrir les différents usages des habitants de Chicago avec pour intuition le fait que les habitants de Chicago utilise majoritairement les vélos pour aller et venir entre leur appartement et leur lieu de travail. Cette même idée mais à Paris est illustrée par ce billet de blog : [Busy areas in Paris](http://www.xavierdupre.fr/blog/2013-09-26_nojs.html).<jupyter_code>from pyensae.datasource import download_data
file = download_data("features_bike_chicago.zip")
file
import pandas
features = pandas.read_csv("features_bike_chicago.txt", sep="\t", encoding="utf-8", low_memory=False, header=[0,1])
features.columns = ["station_id", "station_name", "weekday"] + list(features.columns[3:])
features.head()
features.shape<jupyter_output><empty_output><jupyter_text>Les données sont agrégrées par tranche de 10 minutes soit 144 période durant la journée et 288 nombre pour les départs et arrivées de vélos. Cela explique les dimensions de la matrice.  k-meansOn cherche à trouver différentes zones de la villes pour différents usages. Zones de résidences, zones de travail, zone d'amusements et on suppose que les heures de départs et d'arrivées reflètent ces usages. Dans une zone de travail, typiquement le quartier d'affaires, les vélos arriveront principalement le matin et repartiront le soir. Ce sera l'inverse pour une zone de résidences. C'est pour cela que les arrivées et les départs des vélos ont été agrégés par jour de la semaine. La distribution des arrivées risquent d'être bien différentes le week-end.<jupyter_code>names = features.columns[3:]
from sklearn.cluster import KMeans
clus = KMeans(10)
clus.fit(features[names])
pred = clus.predict(features[names])
set(pred)
features["cluster"] = pred
features[["cluster", "weekday", "station_id"]].groupby(["cluster", "weekday"]).count()
nb = features[["cluster", "weekday", "station_id"]].groupby(["cluster", "weekday"]).count()
nb = nb.reset_index()
nb[nb.cluster.isin([0, 3, 5, 6])].pivot("weekday","cluster", "station_id").plot(kind="bar");<jupyter_output><empty_output><jupyter_text>Let's draw the clusters.<jupyter_code>centers = clus.cluster_centers_.T
import matplotlib.pyplot as plt
fig, ax = plt.subplots(centers.shape[1], 2, figsize=(10,10))
nbf = centers.shape[0] // 2
x = list(range(0,nbf))
col = 0
dec = 0
colors = ["red", "yellow", "gray", "green", "brown", "orange", "blue"]
for i in range(centers.shape[1]):
    if 2*i == centers.shape[1]:
        col += 1
        dec += centers.shape[1]    
    color = colors[i%len(colors)]
    ax[2*i-dec, col].bar (x, centers[:nbf,i], width=1.0, color=color)
    ax[2*i-dec, col].set_ylabel("cluster %d - start" % i, color=color)
    ax[2*i+1-dec, col].bar (x, centers[nbf:,i], width=1.0, color=color)
    ax[2*i+1-dec, col].set_ylabel("cluster %d - stop" % i, color=color)<jupyter_output><empty_output><jupyter_text>On a réussi à isoler plusieurs usages différents. On voit les départs les matin et les arrivées le soir, le modèle inversé, un autre... Mais les-a-t-on tous trouvés ?  Exercice 1 : petits clustersLes petits clusters viennent du fait qu'on impose un nombre de clusters à l'algorithme qui s'efforce de les trouver. Quelques points aberrents sont trop éloignés et isolés du reste des autres. Pour les enlever, on peut simplement réduire le nombre de clusters ou éliminer les points aberrants et les garder comme anomalies. Les petits clusters sont des stations peu utilisées. Le problème quand on calcule une distribution et qu'on la normalise, typiquement celles des arrivées de vélos à une stations, c'est qu'on perd l'information du nombre de vélos ayant servi à l'estimer. L'algorithme [k-means++](https://en.wikipedia.org/wiki/K-means%2B%2B) commence à choisir les points les plus éloignés des uns des autres et cela tombe sur ces stations peu utilisées. Vu le nombre de dimension, 288, les distances sont rapidement très grandes.Une idée consiste à retirer ces stations peu utilisées ou boucler sur un algorithme du type :1. Faire tourner les k-means.2. Retirer les stations dans des clusters isolées.3. Retour à l'étape 1 jusqu'à ce que cela soit interprétable.  Exercice 2 : autres types de clustering<jupyter_code>from sklearn.cluster import DBSCAN
dbs = DBSCAN(eps=0.1)
pred_dbs = dbs.fit_predict(features[names])
set(pred_dbs)
features["cluster_dbs"] = pred_dbs
nbs = features[["cluster_dbs", "weekday", "station_id"]].groupby(["cluster_dbs", "weekday"]).count()
nbs = nbs.reset_index()
nbs[nbs.cluster_dbs.isin([0, 3, 5, 6])].pivot("weekday","cluster_dbs", "station_id").plot(kind="bar");<jupyter_output><empty_output><jupyter_text>L'algorithme [dbscan](http://scikit-learn.org/stable/modules/clustering.htmlclustering) utilise la proximité des points pour classer les observations. On se retrouve dans le cas où il n'y a pas vraiment de frontière entre les clusters et tous les points se retrouvent associés en un unique cluster excepté quelques points aberrants.L'algorithme [DBScan](http://scikit-learn.org/stable/modules/generated/sklearn.cluster.DBSCAN.html) ne fonctionne pas sur ces données. Une des raisons que la distance dans un espace vectoriels avec tant de dimensions n'est pas loin d'une information type binaire : différent partout ou identique presque partout. Qu'un vélo arrive 9h10 ou à 9h20, cela n'a pas beaucoup d'importance pour notre problème, pourtant, la distance choisie lui donnera autant d'importance que si le vélo était arrivé à 10h du soir. Pour éviter cela, il faudrait lisser les distributions avec une moyenne mobile.  GraphesUn peu de code pour voir la réparition des clusters sur une carte.<jupyter_code>piv = features.pivot_table(index=["station_id", "station_name"], 
                           columns="weekday", values="cluster")
piv.head()
piv["distincts"] = piv.apply(lambda row: len(set(row[i] for i in range(0,7))), axis=1)
pivn = piv.reset_index()
pivn.head()
pivn.columns = [str(_).replace(".0", "") for _ in pivn.columns.values]
pivn.head()<jupyter_output><empty_output><jupyter_text>Une carte des stations un jour de semaine.<jupyter_code>from pyensae.datasource import download_data
if False:
    # Provient du site de Chicago
    file = download_data("Divvy_Trips_2016_Q3Q4.zip",
                         url="https://s3.amazonaws.com/divvy-data/tripdata/")
else:
    # Copie au cas où celui-ci tomberait en panne
    file = download_data("Divvy_Trips_2016_Q3.zip")    
stations = pandas.read_csv("Divvy_Stations_2016_Q3.csv")
stations.head()
data = stations.merge(pivn, left_on=["id", "name"],
                      right_on=["station_id", "station_name"], suffixes=('_s', '_c'))
data.sort_values("id").head()
def folium_html_stations_map(stations, html_width=None, html_height=None, **kwargs):
    import folium
    from pyensae.notebookhelper import folium_html_map
    map_osm = None
    for key, value in stations:
        x, y = key
        if map_osm is None:
            if "zoom_start" not in kwargs:
                kwargs["zoom_start"] = 11
            if "location" not in kwargs:
                map_osm = folium.Map(location=[x, y], **kwargs)
            else:
                map_osm = folium.Map(kwargs["location"], **kwargs)
        if isinstance(value, tuple):
            name, value = value
            map_osm.add_child(folium.CircleMarker(
                [x, y], popup=name, radius=15, fill_color=value, color=value))
        else:
            map_osm.add_child(folium.CircleMarker(
                [x, y], radius=15, fill_color=value, color=value))
    return folium_html_map(map_osm, width=html_width, height=html_height)
colors = ["red", "yellow", "gray", "green", "brown", "orange", "blue", "black", "pink", "violet"]
for i, c in enumerate(colors):
    print("Cluster {0} is {1}".format(i, c))
xy = []
for els in data.apply(lambda row: (row["latitude"], row["longitude"], row["1"], row["name"]), axis=1):
    try:
        cl = int(els[2])
    except:
        # NaN
        continue
    name = "%s c%d" % (els[3], cl)
    color = colors[cl]
    xy.append( ( (els[0], els[1]), (name, color)))
folium_html_stations_map(xy, width="80%")<jupyter_output>Cluster 0 is red
Cluster 1 is yellow
Cluster 2 is gray
Cluster 3 is green
Cluster 4 is brown
Cluster 5 is orange
Cluster 6 is blue
Cluster 7 is black
Cluster 8 is pink
Cluster 9 is violet
{'fraction_non_alphanumeric': 0.11241958346963253, 'fraction_numerical': 0.013847999127685095, 'mean_word_length': 4.679256965944273, 'pattern_counts': {'":': 0, '<': 28, '<?xml version=': 0, '>': 28, 'https://': 3, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 5, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '7', 'dir': 'jupyter-structured-clean-dedup', 'id': '331745', 'license': 'MIT', 'n_tokens_mistral': 3140, 'n_tokens_neox': 3034, 'n_words': 948, 'path': '_doc/notebooks/td2a_ml/td2a_clustering_correction.ipynb', 'repo_name': 'Jerome-maker/ensae_teaching_cs', 'size': '9171'}