Améliorations recherche

This commit is contained in:
François Pelletier 2025-01-02 23:04:35 -05:00
parent 731d9bce6d
commit ad0c34fcff
10 changed files with 317 additions and 305 deletions

View file

@ -1,2 +1,5 @@
TYPESENSE_API_KEY= TYPESENSE_API_KEY=RANDOM_STRING_HERE
TYPESENSE_DATA_DIR= TYPESENSE_DATA_DIR=/data
TYPESENSE_HOST=localhost
TYPESENSE_PORT=8108
TYPESENSE_PROTOCOL=http

View file

@ -53,8 +53,9 @@ Tu peux obtenir une sauvegarde des données de tes réseaux sociaux. Je t'ai mis
- Clone le projet avec Git - Clone le projet avec Git
- Configure ta clé API en copiant `.env.template` dans `.env` et en y mettant une clé API de ton choix - Configure ta clé API en copiant `.env.template` dans `.env` et en y mettant une clé API de ton choix
- Configure les autres variables d'environnement dans `.env`.
- Exécute le fichier `docker-compose.yml` avec Docker Compose pour installer le moteur de recherche TypeSense - Exécute le fichier `docker-compose.yml` avec Docker Compose pour installer le moteur de recherche TypeSense
- Connecte-toi à l'application en lançant run_streamlit_app.py et en allant au http://localhost:8501 - Connecte-toi à l'application en lançant run_streamlit_app.py et en allant au http://localhost:8501.
- Si tout fonctionne, tu vas accéder à l'interface de recherche - Si tout fonctionne, tu vas accéder à l'interface de recherche
## Mettre les fichiers au bon endroit ## Mettre les fichiers au bon endroit

View file

@ -0,0 +1,30 @@
import spacy
import subprocess
import sys
def download_spacy_model(model_name):
print(f"Downloading and installing spaCy model: {model_name}")
try:
subprocess.check_call([sys.executable, "-m", "spacy", "download", model_name])
print(f"Successfully installed {model_name}")
except subprocess.CalledProcessError:
print(f"Error installing {model_name}. Please make sure you have the necessary permissions.")
# Download and install English model
download_spacy_model("en_core_web_sm")
# Download and install French model
download_spacy_model("fr_core_news_sm")
# Load the models to verify installation
try:
nlp_en = spacy.load("en_core_web_sm")
print("English model loaded successfully")
except:
print("Error loading English model")
try:
nlp_fr = spacy.load("fr_core_news_sm")
print("French model loaded successfully")
except:
print("Error loading French model")

View file

@ -1,97 +0,0 @@
altair==5.5.0
annotated-types==0.7.0
attrs==24.2.0
av==13.1.0
beautifulsoup4==4.12.3
blinker==1.9.0
blis==1.0.1
cachetools==5.5.0
catalogue==2.0.10
certifi==2024.8.30
charset-normalizer==3.4.0
click==8.1.7
cloudpathlib==0.20.0
coloredlogs==15.0.1
confection==0.1.5
contourpy==1.3.1
ctranslate2==4.5.0
cycler==0.12.1
cymem==2.0.10
en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl#sha256=1932429db727d4bff3deed6b34cfc05df17794f4a52eeb26cf8928f7c1a0fb85
faster-whisper==1.1.0
filelock==3.16.1
flatbuffers==24.3.25
fonttools==4.55.0
fr_core_news_sm @ https://github.com/explosion/spacy-models/releases/download/fr_core_news_sm-3.8.0/fr_core_news_sm-3.8.0-py3-none-any.whl#sha256=7d6ad14cd5078e53147bfbf70fb9d433c6a3865b695fda2657140bbc59a27e29
fsspec==2024.10.0
gitdb==4.0.11
GitPython==3.1.43
huggingface-hub==0.26.3
humanfriendly==10.0
idna==3.10
Jinja2==3.1.4
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
kiwisolver==1.4.7
langcodes==3.5.0
langdetect==1.0.9
language_data==1.3.0
marisa-trie==1.2.1
markdown-it-py==3.0.0
markdownify==0.11.6
MarkupSafe==3.0.2
matplotlib==3.9.3
mdurl==0.1.2
mpmath==1.3.0
murmurhash==1.0.11
narwhals==1.15.0
numpy==2.0.2
onnxruntime==1.20.1
packaging==24.2
pandas==2.2.3
pillow==11.0.0
plotly==5.24.1
preshed==3.0.9
protobuf==5.29.0
pyarrow==17.0.0
pydantic==2.10.2
pydantic_core==2.27.1
pydeck==0.9.1
Pygments==2.18.0
pyparsing==3.2.0
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
pytz==2024.2
PyYAML==6.0.2
referencing==0.35.1
requests==2.31.0
rich==13.9.4
rpds-py==0.21.0
setuptools==75.6.0
shellingham==1.5.4
six==1.16.0
smart-open==7.0.5
smmap==5.0.1
soupsieve==2.6
spacy==3.8.2
spacy-language-detection==0.2.1
spacy-legacy==3.0.12
spacy-loggers==1.0.5
srsly==2.4.8
streamlit==1.40.2
sympy==1.13.3
tenacity==9.0.0
thinc==8.3.2
tokenizers==0.21.0
toml==0.10.2
tornado==6.4.2
tqdm==4.67.1
typer==0.14.0
typesense==0.21.0
typing_extensions==4.12.2
tzdata==2024.2
urllib3==2.2.3
wasabi==1.1.3
weasel==0.4.1
wrapt==1.17.0
xmltodict==0.13.0

View file

@ -1,4 +1,4 @@
import utils.config as config from import_data.utils import config
wordpress_names = config.WORDPRESS_NAMES.split(",") wordpress_names = config.WORDPRESS_NAMES.split(",")
@ -13,5 +13,5 @@ reseau_social_data = [{"nom": "LinkedIn",
{"nom": "FacebookBusiness", {"nom": "FacebookBusiness",
"repertoires": ["posts"]}, "repertoires": ["posts"]},
{"nom": "Podcast", {"nom": "Podcast",
"repertoires": ["shownotes", "audio"]} "repertoires": ["shownotes", "audio", "feeds"]}
] ]

View file

@ -6,10 +6,10 @@ load_dotenv()
client = typesense.Client({ client = typesense.Client({
'nodes': [{ 'nodes': [{
'host': 'localhost', 'host': os.getenv('TYPESENSE_HOST','localhost'),
'port': '8108', 'port': os.getenv('TYPESENSE_PORT','8108'),
'protocol': 'http' 'protocol': os.getenv('TYPESENSE_PROTOCOL','http'),
}], }],
'api_key': os.getenv('TYPESENSE_API_KEY'), 'api_key': os.getenv('TYPESENSE_API_KEY'),
'connection_timeout_seconds': 2 'connection_timeout_seconds': 10
}) })

View file

@ -0,0 +1,40 @@
import streamlit as st
from import_data.utils.typesense_client import client
def rechercher_documents(cette_requete,
ces_filtres=None,
facette_par=None,
query_by="texte,embedding",
sort_by="_text_match:desc,creation_timestamp:desc",
nb_buckets=10,
prefix=False,
per_page=10,
page=1):
parametres_recherche = {
'q': cette_requete,
'query_by': query_by,
'sort_by': sort_by.replace(
"_text_match",
f"_text_match(buckets: {nb_buckets})"),
"exclude_fields": "embedding",
"prefix": str(prefix).lower(),
'per_page': per_page,
'page': page
}
if ces_filtres:
parametres_recherche['filter_by'] = ces_filtres
if facette_par:
parametres_recherche['facet_by'] = facette_par
st.write("Search parameters:", parametres_recherche)
all_results = []
try:
results = client.collections['social_media_posts'].documents.search(parametres_recherche)
all_results.extend(results['hits'])
return results
except Exception as e:
st.error(f"Error during search: {str(e)}") # Error handling

View file

@ -0,0 +1,19 @@
import streamlit as st
from import_data.utils.typesense_client import client
def recuperer_reseaux():
search_parameters = {
'q': '*',
'query_by': 'network',
'facet_by': 'network',
'per_page': 0
}
try:
results = client.collections['social_media_posts'].documents.search(search_parameters)
networks = [facet['value'] for facet in results['facet_counts'][0]['counts']]
return networks
except Exception as e:
st.error(f"Erreur lors de la récupération des réseaux : {str(e)}")
return ['Facebook', 'Instagram', 'Threads', 'LinkedIn', 'WordPress'] # Valeurs par défaut en cas d'erreur

View file

@ -1,134 +1,27 @@
import streamlit as st import streamlit as st
import typesense
from datetime import datetime, time from datetime import datetime, time
import pandas as pd import pandas as pd
import plotly.express as px import plotly.express as px
from dotenv import load_dotenv from dotenv import load_dotenv
import os
from import_data.utils.typesense_client import client
from search_app_ui.rechercher_documents import rechercher_documents
from search_app_ui.recuperer_reseaux import recuperer_reseaux
# Configurer la page en mode large # Configurer la page en mode large
st.set_page_config(layout="wide") st.set_page_config(layout="wide")
# Forcer le thème sombre # Load and apply the CSS
st.markdown(""" def load_css(file_name):
<style> with open(file_name) as f:
/* Fond principal */ st.markdown(f'<style>{f.read()}</style>', unsafe_allow_html=True)
.stApp {
background-color: #0e1117;
color: #fafafa;
}
/* Barre latérale */
.css-1d391kg {
background-color: #262730;
}
/* Boutons */
.stButton>button {
color: #fafafa;
background-color: #262730;
border-color: #fafafa;
}
/* Champs de texte */
.stTextInput>div>div>input {
color: #fafafa;
background-color: #262730;
}
/* Boîte de sélection */
.stSelectbox>div>div>select {
color: #fafafa;
background-color: #262730;
}
/* Sélection multiple */
.stMultiSelect>div>div>select {
color: #fafafa;
background-color: #262730;
}
/* Saisie de date */
.stDateInput>div>div>input {
color: #fafafa;
background-color: #262730;
}
</style>
""", unsafe_allow_html=True)
# Ajouter ce CSS pour créer une zone de résultats défilable # Load the CSS file
st.markdown(""" load_css('style.css') # If the CSS file is in a subdirectory, adjust the path accordingly
<style>
.scrollable-results {
height: 400px;
overflow-y: scroll;
border: 1px solid #ccc;
padding: 10px;
border-radius: 5px;
}
</style>
""", unsafe_allow_html=True)
# Charger les variables d'environnement # Charger les variables d'environnement
load_dotenv() load_dotenv()
# Initialiser le client Typesense
client = typesense.Client({
'nodes': [{
'host': 'localhost',
'port': '8108',
'protocol': 'http'
}],
'api_key': os.getenv('TYPESENSE_API_KEY'),
'connection_timeout_seconds': 2
})
def rechercher_documents(cette_requete, ces_filtres=None, facette_par=None):
parametres_recherche = {
'q': cette_requete,
'query_by': 'texte,embedding',
'sort_by': '_text_match(buckets: 10):desc,creation_timestamp:desc',
"exclude_fields": "embedding",
"prefix": "false",
'per_page': 10,
'page': 1
}
if ces_filtres:
parametres_recherche['filter_by'] = ces_filtres
if facette_par:
parametres_recherche['facet_by'] = facette_par
st.write("Search parameters:", parametres_recherche)
all_results = []
try:
while True:
results = client.collections['social_media_posts'].documents.search(parametres_recherche)
all_results.extend(results['hits'])
if len(all_results) >= results['found']:
break
parametres_recherche['page'] += 1
results['hits'] = all_results
return results
except Exception as e:
st.error(f"Error during search: {str(e)}") # Error handling
# Récupérer dynamiquement les réseaux depuis Typesense
def get_networks():
search_parameters = {
'q': '*',
'query_by': 'network',
'facet_by': 'network',
'per_page': 0
}
try:
results = client.collections['social_media_posts'].documents.search(search_parameters)
networks = [facet['value'] for facet in results['facet_counts'][0]['counts']]
return networks
except Exception as e:
st.error(f"Erreur lors de la récupération des réseaux : {str(e)}")
return ['Facebook', 'Instagram', 'Threads', 'LinkedIn', 'WordPress'] # Valeurs par défaut en cas d'erreur
# Interface utilisateur Streamlit # Interface utilisateur Streamlit
st.title('Recherche dans tes contenus publiés sur le web') st.title('Recherche dans tes contenus publiés sur le web')
@ -147,7 +40,7 @@ date_fin = col2.date_input('Date de fin', value=datetime.now())
# Filtre de réseau social et de langue # Filtre de réseau social et de langue
col3, col4 = st.columns(2) col3, col4 = st.columns(2)
reseaux = get_networks() reseaux = recuperer_reseaux()
reseaux_selectionnes = col3.multiselect('Sélectionnez les réseaux sociaux', reseaux, reseaux_selectionnes = col3.multiselect('Sélectionnez les réseaux sociaux', reseaux,
default=reseaux[0] if reseaux else None) default=reseaux[0] if reseaux else None)
langues = [('fr', 'Français'), ('en', 'English')] langues = [('fr', 'Français'), ('en', 'English')]
@ -156,105 +49,187 @@ langue_selectionnees = col4.multiselect('Sélectionnez la langue',
format_func=lambda x: x, format_func=lambda x: x,
default='Français') default='Français')
# Filtre sur le nombre de mots
nombre_mots = st.slider('Nombre de mots minimum', min_value=0, max_value=1000, value=100, step=10)
# Convertir les étiquettes en codes de langage # Convertir les étiquettes en codes de langage
selected_lang_codes = [code for code, label in langues if label in langue_selectionnees] selected_lang_codes = [code for code, label in langues if label in langue_selectionnees]
# Filtre sur le nombre de mots # Nouvelle section pour les options de recherche avancées
nombre_mots = st.slider('Nombre de mots minimum', min_value=0, max_value=1000, value=100, step=10) st.sidebar.header("Options de recherche avancées")
# Option pour activer/désactiver les options avancées
show_advanced_options = st.sidebar.checkbox("Activer les options avancées")
if show_advanced_options:
# Champs de recherche
query_by = st.sidebar.multiselect(
"Champs de recherche",
["texte", "embedding"],
default=["texte", "embedding"]
)
# Tri
sort_options = [
"_text_match:desc", "creation_timestamp:desc",
"_text_match:asc", "creation_timestamp:asc",
]
nb_buckets = st.sidebar.number_input("Nombre de buckets pour le tri", min_value=1, value=10)
sort_by = st.sidebar.multiselect(
"Trier par",
sort_options,
default=["_text_match:desc", "creation_timestamp:desc"]
)
# Préfixe
prefix = st.sidebar.checkbox("Activer la recherche par préfixe", value=False)
# Pagination
per_page = st.sidebar.slider("Résultats par page", min_value=1, max_value=100, value=10)
page = st.sidebar.number_input("Page", min_value=1, value=1)
else:
# Valeurs par défaut si les options avancées ne sont pas affichées
query_by = ["texte", "embedding"]
sort_by = ["_text_match:desc", "creation_timestamp:desc"]
prefix = False
nb_buckets = 10
per_page = 10
page = 1
if st.button('Rechercher'): if st.button('Rechercher'):
# Préparer les filtres # Préparer les filtres
debut_datetime = datetime.combine(date_debut, time.min) debut_datetime = datetime.combine(date_debut, time.min)
fin_datetime = datetime.combine(date_fin, time.max) fin_datetime = datetime.combine(date_fin, time.max)
filtre_date = f"creation_timestamp:[{int(debut_datetime.timestamp())}..{int(fin_datetime.timestamp())}]" filtre_date = f"creation_timestamp:[{int(debut_datetime.timestamp())}..{int(fin_datetime.timestamp())}]"
filtre_reseau = f"network:[{' '.join(reseaux_selectionnes)}]" if reseaux_selectionnes else None filtre_reseau = f"network:=[{', '.join(reseaux_selectionnes)}]" if reseaux_selectionnes else None
filtre_langue = f"langue:[{' '.join(selected_lang_codes)}]" if selected_lang_codes else None filtre_langue = f"langue:=[{', '.join(selected_lang_codes)}]" if selected_lang_codes else None
filtre_mots = f"nombre_de_mots:[{nombre_mots}..10000]" if nombre_mots > 0 else None filtre_mots = f"nombre_de_mots:[{nombre_mots}..10000]" if nombre_mots > 0 else None
filtres = ' && '.join(filter(None, [filtre_date, filtre_reseau, filtre_langue, filtre_mots])) liste_filtres = []
if filtre_date:
liste_filtres.append(filtre_date)
if filtre_reseau:
liste_filtres.append(filtre_reseau)
if filtre_langue:
liste_filtres.append(filtre_langue)
if filtre_mots:
liste_filtres.append(filtre_mots)
filtres = ' && '.join(liste_filtres) if liste_filtres else None
# Effectuer la recherche pour tous les résultats # Effectuer la recherche pour tous les résultats
tous_resultats = rechercher_documents(requete, ces_filtres=filtres, facette_par='network') tous_resultats = rechercher_documents(
nombre_total_resultats = tous_resultats['found'] requete,
ces_filtres=filtres,
facette_par='network',
query_by=','.join(query_by),
sort_by=','.join(sort_by),
nb_buckets=nb_buckets,
prefix=prefix,
per_page=per_page,
page=page
)
if tous_resultats:
nombre_total_resultats = tous_resultats['found']
# Afficher le nombre total de résultats
st.subheader(f"Trouvé {nombre_total_resultats} résultats parmi {total_documents } documents indexés")
else:
nombre_total_resultats = 0
st.subheader("Aucun résultat trouvé")
# Afficher le nombre total de résultats
st.subheader(f"Trouvé {nombre_total_resultats} résultats parmi {total_documents } documents indexés")
# Affichage des résultats (100 maximum)
st.subheader("Résultats de la recherche")
for hit in tous_resultats['hits'][:100]: # Limite à 100 résultats
col1, col2 = st.columns([1, 4])
with col1:
st.markdown(f"**{hit['document']['network']}**")
st.markdown(
f"**{datetime.fromtimestamp(hit['document']['creation_timestamp']).strftime('%Y-%m-%d %H:%M:%S')}**")
st.markdown(f"**{hit['document']['nombre_de_mots']} mots**")
# Score
st.markdown(f"**Score: {hit["hybrid_search_info"]['rank_fusion_score']}**")
# Étiquettes de couleur pour les facettes
st.markdown(f"""
<span style="background-color: #007bff; color: white; padding: 2px 6px; border-radius: 10px;">
{hit['document']['langue']}
</span>
""", unsafe_allow_html=True)
with col2:
# Boîte de texte pour le contenu
st.text_area("Contenu", hit['document']['texte'], height=150)
# URI en dessous
if 'uri' in hit['document']:
st.markdown(f"[Lien vers le post original]({hit['document']['uri']})")
st.markdown("---")
# Afficher les facettes
if 'facet_counts' in tous_resultats:
facettes_reseau = {facette['value']: facette['count'] for facette in
tous_resultats['facet_counts'][0]['counts']}
st.subheader("Résultats par Réseau")
# Graphique en camembert pour montrer la distribution des résultats par réseau social
fig = px.pie(values=list(facettes_reseau.values()), names=list(facettes_reseau.keys()),
title="Distribution par Réseau")
# Ce graphique montre la proportion de résultats pour chaque réseau social
st.plotly_chart(fig)
# Distribution temporelle par réseau et par mois
if nombre_total_resultats > 0: if nombre_total_resultats > 0:
st.subheader("Résultats au fil du temps par réseau (agrégation mensuelle)") # Affichage des résultats (100 maximum)
st.subheader("Résultats de la recherche")
df_temporel = pd.DataFrame({ for hit in tous_resultats['hits'][:100]: # Limite à 100 résultats
'date': [datetime.fromtimestamp(hit['document']['creation_timestamp']) for hit in tous_resultats['hits']], col1, col2 = st.columns([1, 4])
'network': [hit['document']['network'] for hit in tous_resultats['hits']]
})
df_temporel['mois'] = df_temporel['date'].dt.to_period('M') with col1:
df_temporel = df_temporel.groupby(['mois', 'network']).size().reset_index(name='count') st.markdown(f"**{hit['document']['network']}**")
df_temporel['mois'] = df_temporel['mois'].dt.to_timestamp() st.markdown(
f"**{datetime.fromtimestamp(hit['document']['creation_timestamp']).strftime('%Y-%m-%d %H:%M:%S')}**")
st.markdown(f"**{hit['document']['nombre_de_mots']} mots**")
# Score
if "texte" in query_by and "embedding" in query_by:
score = hit["hybrid_search_info"]['rank_fusion_score']
score_type = "Hybrid"
elif "texte" in query_by:
score = hit["text_match_info"]['score']
score_type = "Text"
elif "embedding" in query_by:
score = hit["vector_distance"]
score_type = "Vector"
else:
score = "N/A"
score_type = "Unknown"
st.markdown(f"**{score_type} Score: {score}**")
# Étiquettes de couleur pour les facettes
st.markdown(f"""
<span style="background-color: #007bff; color: white; padding: 2px 6px; border-radius: 10px;">
{hit['document']['langue']}
</span>
""", unsafe_allow_html=True)
# Graphique linéaire pour montrer l'évolution du nombre de posts par réseau au fil du temps with col2:
fig = px.line(df_temporel, x='mois', y='count', color='network', # Boîte de texte pour le contenu
title="Distribution temporelle par réseau (agrégation mensuelle)") st.text_area("Contenu", hit['document']['texte'], height=150)
fig.update_layout(xaxis_title="Mois", yaxis_title="Nombre de posts")
fig.update_xaxes(tickformat="%B %Y")
# Ce graphique permet de visualiser les tendances de publication pour chaque réseau social au fil des mois
st.plotly_chart(fig)
# Graphique à barres empilées pour montrer la répartition des posts par réseau pour chaque mois # URI en dessous
fig_bar = px.bar(df_temporel, x='mois', y='count', color='network', if 'uri' in hit['document']:
title="Distribution temporelle par réseau (barres empilées, agrégation mensuelle)") st.markdown(f"[Lien vers le post original]({hit['document']['uri']})")
fig_bar.update_layout(xaxis_title="Mois", yaxis_title="Nombre de posts") # Affichage du hit brut
fig_bar.update_xaxes(tickformat="%B %Y") st.subheader("Données brutes")
# Ce graphique permet de comparer facilement le volume de posts entre les différents réseaux sociaux pour chaque mois st.json(hit, expanded=False)
st.plotly_chart(fig_bar)
st.subheader("Tableau récapitulatif mensuel") st.markdown("---")
df_pivot = df_temporel.pivot(index='mois', columns='network', values='count').fillna(0)
df_pivot['Total'] = df_pivot.sum(axis=1) # Afficher les facettes
df_pivot = df_pivot.reset_index() if 'facet_counts' in tous_resultats:
df_pivot['mois'] = df_pivot['mois'].dt.strftime('%B %Y') facettes_reseau = {facette['value']: facette['count'] for facette in
# Ce tableau fournit un résumé détaillé du nombre de posts par réseau social pour chaque mois tous_resultats['facet_counts'][0]['counts']}
st.dataframe(df_pivot) st.subheader("Résultats par Réseau")
# Graphique en camembert pour montrer la distribution des résultats par réseau social
fig = px.pie(values=list(facettes_reseau.values()), names=list(facettes_reseau.keys()),
title="Distribution par Réseau")
# Ce graphique montre la proportion de résultats pour chaque réseau social
st.plotly_chart(fig)
# Distribution temporelle par réseau et par mois
if nombre_total_resultats > 0:
st.subheader("Résultats au fil du temps par réseau (agrégation mensuelle)")
df_temporel = pd.DataFrame({
'date': [datetime.fromtimestamp(hit['document']['creation_timestamp']) for hit in tous_resultats['hits']],
'network': [hit['document']['network'] for hit in tous_resultats['hits']]
})
df_temporel['mois'] = df_temporel['date'].dt.to_period('M')
df_temporel = df_temporel.groupby(['mois', 'network']).size().reset_index(name='count')
df_temporel['mois'] = df_temporel['mois'].dt.to_timestamp()
# Graphique linéaire pour montrer l'évolution du nombre de posts par réseau au fil du temps
fig = px.line(df_temporel, x='mois', y='count', color='network',
title="Distribution temporelle par réseau (agrégation mensuelle)")
fig.update_layout(xaxis_title="Mois", yaxis_title="Nombre de posts")
fig.update_xaxes(tickformat="%B %Y")
# Ce graphique permet de visualiser les tendances de publication pour chaque réseau social au fil des mois
st.plotly_chart(fig)
# Graphique à barres empilées pour montrer la répartition des posts par réseau pour chaque mois
fig_bar = px.bar(df_temporel, x='mois', y='count', color='network',
title="Distribution temporelle par réseau (barres empilées, agrégation mensuelle)")
fig_bar.update_layout(xaxis_title="Mois", yaxis_title="Nombre de posts")
fig_bar.update_xaxes(tickformat="%B %Y")
# Ce graphique permet de comparer facilement le volume de posts entre les différents réseaux sociaux pour chaque mois
st.plotly_chart(fig_bar)
st.subheader("Tableau récapitulatif mensuel")
df_pivot = df_temporel.pivot(index='mois', columns='network', values='count').fillna(0)
df_pivot['Total'] = df_pivot.sum(axis=1)
df_pivot = df_pivot.reset_index()
df_pivot['mois'] = df_pivot['mois'].dt.strftime('%B %Y')
# Ce tableau fournit un résumé détaillé du nombre de posts par réseau social pour chaque mois
st.dataframe(df_pivot)

41
search_app_ui/style.css Normal file
View file

@ -0,0 +1,41 @@
/* Fond principal */
.stApp {
background-color: #0e1117;
color: #fafafa;
}
/* Barre latérale */
.css-1d391kg {
background-color: #262730;
}
/* Boutons */
.stButton>button {
color: #fafafa;
background-color: #262730;
border-color: #fafafa;
}
/* Champs de texte */
.stTextInput>div>div>input {
color: #fafafa;
background-color: #262730;
}
/* Boîte de sélection */
.stSelectbox>div>div>select {
color: #fafafa;
background-color: #262730;
}
/* Sélection multiple */
.stMultiSelect>div>div>select {
color: #fafafa;
background-color: #262730;
}
/* Saisie de date */
.stDateInput>div>div>input {
color: #fafafa;
background-color: #262730;
}