%matplotlib inline
from matplotlib import pyplot as plt
from matplotlib import cm
import pandas as pd
import numpy as np
import seaborn as sns
import datetime as dt
from tabulate import tabulate
import re
import math
sns.set_palette("deep", desat=.6)
sns.set_context(rc={"figure.figsize": (16, 6), "lines.linewidth": 1.5}, font_scale=2)
# Load data
content = pd.read_pickle("data/content_04012015-04032015.pkl")
content['id'] = content.index
content["published"] = content.url.apply( \
lambda x: re.match(".*\/(?P<date>[0-9]{4}\/[0-9]{2}\/[0-9]{2}).*\.html", x).group("date"))
hits = pd.read_pickle("data/hits_04012015-04032015.pkl")
refday = dt.date(2015,3,4)
refday_col = refday.strftime("%Y_%m_%d")
# Utilities
def get_timeline(hits, id, length = 60):
tl = hits.loc[id]
tl.index = [dt.datetime.strptime(x, "%Y_%m_%d").date() for x in tl.index]
tl = tl.sort_index()
last = tl.index[-1]
return tl[last - dt.timedelta(days=length):last].sort_index()
def tmplot(aids, hits, content, ymax=0, fs=(17,6), title="", ylabel="", xlabel="", lgy=False):
plt.figure(figsize=fs)
cs=cm.Accent(np.linspace(0,1,len(aids)))
for idx, aid in enumerate(aids):
tm = get_timeline(hits, aid)
info = content.loc[aid]
tm.plot(label='{} - {} ({}, {})'.format(info.rubrique, info.title, info.published, info.id), c=cs[idx], logy=lgy)
if ymax: plt.ylim(0,ymax)
if title: plt.title(title, fontsize=16, y=1.07)
if ylabel: plt.ylabel(ylabel)
if xlabel: plt.xlabel(xlabel)
plt.legend()
plt.show()
def list_content(clist, size=10):
def strtrunc(x, l):
if len(x) > l: return x[:l] + "…"
else: return x
data = clist.head(size).copy()
data.title = data.title.apply(strtrunc, l=38)
data.rubrique = data.rubrique.apply(strtrunc, l=13)
data.index = data.id.apply(str)
print(tabulate(data[["title", "rubrique", "published", "hits"]], headers=["TITLE", "RUBRIQUE", "DATE PUB.", "VIS."]))
return
« Quality, useful content that is relevant to readers for a long period of time. »
« [Content that] can be linked to and gain traffic long after it is originally published. »
« Will people still read this and think it’s interesting a year from now ? »
La majorité du contenu du Monde est anti-evergreen par définition :
ou AT internet : c'est d'la balle !
aids = [4569221, 4549206, 4549610, 4569862]
tmplot(aids, hits, content, fs=(10,7), title="La vie de quelques articles", ylabel="Visites", xlabel="Dates")
# Subsample daily data
daydata = hits[refday_col].copy()
daydata.name = "hits"
daydata = daydata[daydata > 0]
daycontent = pd.merge(content.copy(), pd.DataFrame(daydata), left_index=True, right_index=True, how="inner")
daycontent["age"] = daycontent.published.apply( \
lambda x: (refday - dt.datetime.strptime(x, "%Y/%m/%d").date()).days)
daycontent["published_dt"] = daycontent.published.apply( \
lambda x: dt.datetime.strptime(x, "%Y/%m/%d").date())
print("Total articles read: ", len(daycontent))
print("Total views: ", int(daydata.sum()))
print("Total rubriques uniques: ", len(daycontent.rubrique.unique()))
print("Âges: du jour-même à plusieurs années.")
Total articles read: 48241 Total views: 1711346 Total rubriques uniques: 237 Âges: du jour-même à plusieurs années.
list_content(daycontent.sort(columns="hits", ascending=False))
TITLE RUBRIQUE DATE PUB. VISITES ------- --------------------------------------- ----------- ----------- --------- 4586979 genevieve_fioraso_veut_quitter_le_gouv… politique 2015/03/04 94344 4587221 pour_taubira_les_propos_du_maire_de_to… politique 2015/03/04 50700 4587403 nouveaux_remous_au_world_press_photo arts 2015/03/04 43352 4586967 trois_suspects_identifies_trente_trois… societe 2015/03/04 42673 4586674 detecteur_de_fumee_plus_que_cinq_jours… immobilier 2015/03/03 38451 4586182 un_requin_lutin_peche_au_large_de_l_au… planete 2015/03/03 36053 4586892 paris_a_nouveau_survolee_par_une_dizai… societe 2015/03/04 29530 4587484 decouverte_exceptionnelle_en_france_d_… archeologie 2015/03/04 29413 4586215 le_penis_de_long_en_large sciences 2015/03/03 28848 4586899 areva_annonce_un_plan_d_un_milliard_d_… economie 2015/03/04 28110
Comment les visites sont-elles distribuées à travers l'ensemble des articles lus dans une journée ?
sorted_hits = daycontent.sort(ascending=False, columns="hits").hits.values
plt.figure(figsize=(16,5))
plt.plot(sorted_hits)
plt.title("Distribution des visites par article le 4/3/2015", fontsize=14)
plt.ylabel("Visites")
plt.xlim(0,1000)
plt.xlabel("Les 1000 articles les plus vus (sur un total de {})".format(len(sorted_hits)))
plt.axvline(200, label="à droite : moins de 873 visites", color="blue")
plt.axvline(600, label="à droite : moins de 149 visites", color="red")
plt.legend()
plt.show()
On peut s'en servir pour augmenter le trafic global sur le site :
Pour cela, il faut qu'on connaisse un peu mieux le contenu de cette longue traîne
max = 60000
min = 1
vals = daycontent.hits.values
plt.figure(figsize=(16,4))
n, bins, patches = plt.hist(vals, log=True, bins = 10 ** np.linspace(np.log10(min), np.log10(max), 10))
plt.gca().set_xscale("log")
plt.gca().set_yscale("log")
plt.xlim(0,max)
plt.ylim(1,100000)
plt.title('Fréquences des visites sur un jour', fontsize=14)
plt.xlabel('Visites (log)')
plt.ylabel("Nombre d'articles (log)")
plt.show()
n_nb = len(n)
total_n = sum(n)
total_v = sum(vals)
bin_count_p_c = 1
views_p_c = 1
views_p_prev = 0
bin_count_p_prev = 0
res = []
for i in range(0,n_nb):
low = math.floor(bins[i])
if low != 1:
low += 1
if i < n_nb - 1:
high = math.floor(bins[i+1])
else:
high = vals.max()
bin_count = int(n[i])
bin_count_p = n[i]/total
views = np.sum(vals[np.logical_and(vals >= low, vals <= high)])
views_p = views/total_v
views_p_c -= views_p_prev
bin_count_p_c -= bin_count_p_prev
res.append(("{:.0f} - {:.0f}".format(low, high), \
int(n[i]), \
"{0:.2%}".format(bin_count_p), \
"{0:.2%}".format(views_p), \
"{0:.2%}".format(bin_count_p_c), \
"{0:.2%}".format(views_p_c)))
views_p_prev = views_p
bin_count_p_prev = bin_count_p
print(tabulate(res[::-1], headers=["VISITES", "# ART.", "% ART.", "% VIS.", "% ART. TOTAL", "% V. TOTAL"], stralign="right"))
VISITES # ART. % ART. % VIS. % ART. TOTAL % V. TOTAL ------------- -------- -------- -------- -------------- ------------ 17671 - 94344 21 0.04% 40.23% 0.04% 40.23% 5205 - 17670 43 0.09% 20.60% 0.13% 60.83% 1533 - 5204 81 0.17% 14.14% 0.30% 74.97% 452 - 1532 155 0.32% 7.37% 0.62% 82.34% 133 - 451 339 0.70% 4.78% 1.32% 87.12% 40 - 132 872 1.81% 3.47% 3.13% 90.59% 12 - 39 2848 5.90% 3.38% 9.04% 93.97% 4 - 11 8427 17.47% 3.01% 26.50% 96.98% 1 - 3 35454 73.50% 3.02% 100.00% 100.00%
Prenons les articles du top 2% :
# sub5p : 5% articles with the most views (2346 of 48241)
q = int(daycontent.hits.quantile(0.98))
subp = daycontent[daycontent.hits > q]
print("Top 2% articles ({}) have {:.2%} of the views".format(len(subp), subp.hits.sum()/daycontent.hits.sum()))
print("All the other 98% have less than {:.0f} views".format(subp.hits.min()))
Top 2% articles (965) have 88.91% of the views All the other 98% have less than 72 views
plt.figure(figsize=(16,4))
n, bins, patches = plt.hist(subp.age.values[subp.age.values < 1000], log=True, bins=100)
plt.title('Fréquences des ages des articles du top 2%', fontsize=14)
plt.xlabel('Nombre de jours après publication')
plt.ylabel("Nombre d'articles (log)")
plt.show()
bin_nb = len(n)
article_nb = sum(n)
total_views = daycontent.hits.sum()
res = []
for i in range(0,bin_nb):
low = math.floor(bins[i])
high = math.floor(bins[i+1])
if low != 0:
low += 1
bin_count = int(n[i])
bin_count_p = n[i]/article_nb
bin_views_p = daycontent[(daycontent.age >= low) & (daycontent.age <= high)].hits.sum() / total_views
res.append(("{:.0f} - {:.0f}".format(low, high), \
int(n[i]), \
"{0:.2%}".format(bin_count_p), \
"{0:.2%}".format(bin_views_p)))
if i > 5: break
print(tabulate(res, headers=["AGE (JOURS)", "# ART.", "% ART.", "% VUES"]))
print("...")
AGE (JOURS) # ART. % ART. % VUES ------------- -------- -------- -------- 0 - 9 598 63.28% 83.67% 10 - 19 87 9.21% 2.47% 20 - 29 65 6.88% 1.69% 30 - 39 12 1.27% 0.53% 40 - 49 29 3.07% 0.66% 50 - 59 12 1.27% 0.45% 60 - 69 8 0.85% 0.44% ...
Un tel site coûte horriblement cher à produire : des contenus "jetables", éphémères
On pourrait mitiger cette dépendance à la production quotidienne avec un peu de Evergreen
Essayons de trouver ces « vieux » articles qui « rapportent » toujours.
list_content(subp[subp.age > 90].sort(columns="hits", ascending=False), size=20)
TITLE RUBRIQUE DATE PUB. VIS. ------- --------------------------------------- -------------- ----------- ------ 4525701 ce_que_changera_la_nouvelle_prime_d_ac… les-decodeurs 2014/11/19 7518 1610778 pour_les_agriculteurs_ressemer_sa_prop… planete 2011/11/29 5002 1708654 christine_lagarde_non_plus_ne_paie_pas… europe 2012/05/28 944 4463702 pas_si_facile_de_lutter_contre_l_evasi… les-decodeurs 2014/07/28 776 4442319 au_fait_quelle_difference_entre_sunnit… les-decodeurs 2014/06/20 749 3415091 ecole_primaire_et_secondaire ecole-primair… 2013/05/22 727 4473771 fiscalite_ce_que_signifient_les_annonc… les-decodeurs 2014/08/20 556 1331612 moi_claude_dilain_maire_de_clichy_sous… societe 2010/04/10 549 4525745 areva_dans_la_tourmente economie 2014/11/19 547 1574837 le_cochon_de_gaza_une_farce_ni_casher_… cinema 2011/09/20 522
arts = [4442319, 1708654, 4467506, 1331612]
tmplot(arts, hits, content, \
title="Visites quotidiennes entre le 3 janvier et le 4 mars 2015", \
ylabel="Visites", \
xlabel="Dates")
Si on a accès à la timeline d'un content, on peut élaborer une définition a posteriori d'evergreen.
Un article est evergreen sur une période donnée s'il:
fake_eg = 1610778
info = content.loc[fake_eg]
tm = get_timeline(hits, fake_eg, 30)
f, axarr = plt.subplots(1,2, figsize=(17, 5))
f.suptitle('Un fake evergreen : {}… ({}, {})'.format(info.title[:50] , info.rubrique, info.published), fontsize=16, y=1.05)
tm.plot(ax=axarr[0], title="timeline sur 30 jours")
axarr[0].set_ylabel("visites")
tm_mean = tm.mean()
tm_median = np.median(tm.values)
axarr[1].hist(tm.values, bins=20)
axarr[1].set_title('Fréquences des visites')
axarr[1].set_xlabel('Visites')
axarr[1].axvline(tm_mean, c='b', label="Moyenne = {:.0f}".format(tm_mean))
axarr[1].axvline(tm_median, c='r', label="Médiane = {:.0f}".format(tm_median))
axarr[1].legend()
plt.show()
Pour les distributions biaisées, la médiane est une meilleure mesure de la tendance centrale que la moyenne.
aid = 4442319
info = content.loc[aid]
tm = get_timeline(hits, aid, 30)
f, axarr = plt.subplots(1,2, figsize=(17, 5))
f.suptitle('Un vrai evergreen : {}… ({}, {})'.format(info.title[:50] , info.rubrique, info.published), fontsize=16, y=1.05)
tm.plot(ax=axarr[0], title="Timeline sur 30 jours")
axarr[0].set_ylabel("visites")
tm_mean = tm.mean()
tm_median = np.median(tm.values)
axarr[1].hist(tm.values, bins=20)
axarr[1].set_title('Fréquences des visites')
axarr[1].set_xlabel('Visites')
axarr[1].axvline(tm_mean, c='b', label="Moyenne = {:.0f}".format(tm_mean))
axarr[1].axvline(tm_median, c='r', label="Médiane = {:.0f}".format(tm_median))
axarr[1].legend()
plt.show()
def is_evergreen(info, views, refday, minday, threshold = 300):
tm = get_timeline(views, info.id)
val = tm[minday:refday].median()
return val > threshold and info.published_dt < minday
daycontent['evergreen'] = daycontent.apply( \
is_evergreen, refday = refday, minday = refday - dt.timedelta(days=30), \
views = hits, threshold=200, axis=1)
list_content(daycontent[daycontent['evergreen']].sort(columns="hits", ascending=False), size=11)
TITLE RUBRIQUE DATE PUB. VIS. ------- --------------------------------------- -------------- ----------- ------ 1708654 christine_lagarde_non_plus_ne_paie_pas… europe 2012/05/28 944 4442319 au_fait_quelle_difference_entre_sunnit… les-decodeurs 2014/06/20 749 4555180 de_charlie_a_dieudonne_jusqu_ou_va_la_… les-decodeurs 2015/01/14 577 1717261 merah_aurait_decouvert_qu_il_etait_man… societe 2012/06/12 421 4560905 une_nouvelle_etude_accable_la_cigarett… sante 2015/01/22 403 4542278 la_carte_a_13_regions_definitivement_a… politique 2014/12/17 309 4335033 trois_questions_sur_la_hausse_du_smic societe 2013/12/16 288 4408809 tout_comprendre_sur_votre_fiche_de_pai… les-decodeurs 2014/04/29 286 4567091 marine_le_pen_en_tete_en_2017_des_sond… politique 2015/01/30 239 4554839 c_est_charlie_venez_vite_ils_sont_tous… societe 2015/01/13 198 4353582 en_inde_une_femme_condamnee_a_un_viol_… asie-pacifiqu… 2014/01/23 195
Critère : médiane de plus de 300 visites, sur un mois.
Cette liste comprend deux types d'articles : les vrais-de-vrai, et ceux qui ont du potentiel.
aids = [4442319, 4353582]
tmplot(aids, hits, content, title="Deux articles vrais-de-vrai evergreen", ylabel="Visites", xlabel="Dates")
aids = [4555180, 4560905]
tmplot(aids, hits, content, title="Deux articles ayant du potentiel evergreen", ylabel="Visites (log)", xlabel="Dates", lgy=True)
Case study: Refreshing the Evergreen
Il y a déjà de quoi faire un petit prototype non ?