انتقل إلى المحتوى الرئيسي

الوحدة 1 — الخطوات الأولى ونموذج تنفيذ Streamlit

قبل أوّل مكوّن، وقبل أوّل رسم، لا بدّ من فهم مسألة واحدة تُميّز Streamlit عن كلّ إطار ويب تعرّفت عليه: السكربت يُعاد تنفيذه من أعلى إلى أسفل عند كلّ تفاعل. هذه الجملة تبدو تفصيلًا تقنيًّا، لكنّها تُملي شكل الكود كلّه، وتُفسّر كلّ نمط سترى فيما بعد — من التخزين المؤقّت، إلى session_state، إلى النماذج، إلى فصل الصفحات. لا يمكن التصميم ضدّ هذا النموذج؛ يجب التصميم بحسبه.

التثبيت وأوّل تطبيق

نبدأ ببيئة نظيفة. Streamlit يشتغل على بايثون 3.9 فما فوق، ولا يحتاج شيئًا آخر. من سطر الأوامر:

python -m venv .venv
source .venv/bin/activate # على ويندوز : .venv\Scripts\activate
pip install "streamlit>=1.30" pandas scikit-learn

بعدها، سطر أوّليّ في ملفّ app.py يكفي للتحقّق من أنّ كلّ شيء يعمل:

import streamlit as st

st.title("لوحة قيادة ترك العملاء")
st.write("مرحبًا! هذا أوّل تطبيق يعمل.")

نُطلق التطبيق من الطرفيّة:

streamlit run app.py

يفتح المتصفّح على http://localhost:8501، ونرى العنوان والجملة. لا خادم Flask، لا templates/، لا نقاط نهاية REST مكتوبة يدويًّا. الملفّ الواحد هو الواجهة.

قاعدة التنفيذ الأساسيّة

كلّ مرّة يضغط المستخدم زرًّا، أو يُغيّر منزلقًا، أو يختار عنصرًا من قائمة، يُعاد تنفيذ الملفّ كاملًا من أعلى إلى أسفل. لا استماع لأحداث، لا onChange مثل React، لا حلقة تفاعل. Streamlit يُنفّذ الملفّ، ويلتقط ما تعرضه دوالّه (st.title، st.dataframe، st.plotly_chart)، ويرسم واجهة جديدة كاملة تحلّ محلّ السابقة.

هذا يخالف الحدس القادم من التطوير الويب الكلاسيكيّ. لكنّه يوافق تمامًا حدس عالِم البيانات: كأنّ السكربت يُعاد تشغيله كلّ مرّة، مع فارق أنّ Streamlit يتذكّر ما يجب تذكّره (بـcache_data وsession_state، وستأتي الوحدتان الخامسة والسادسة عليهما). الفكرة أنّ الشيفرة تبقى قصيرة وخطّيّة، بلا انتشار حالات موزّعة عبر مستمعي أحداث متفرّقين.

مثال ملموس: إذا كتبت x = st.slider("العدد", 0, 100, 50)، فقيمة x تُحسب في كلّ تنفيذ. أوّل تنفيذ يعطيها 50 (القيمة الافتراضيّة). حين يُحرِّك المستخدم المنزلق إلى 70، يُعاد تنفيذ الملفّ كاملًا، وتُصبح x تساوي 70. كلّ ما يلي هذا السطر يُحسب من جديد باستعمال 70. لا مستمع، لا رسالة، لا معالجة يدويّة.

نتائجه على التصميم

ثلاث نتائج مباشرة يجب استيعابها من الآن، وإلّا تعثّرت في الوحدات التالية:

أوّلًا: كلّ ما يبطئ يجب أن يُخزَّن مؤقّتًا. قراءة ملفّ CSV بحجم 20 ميغابايت في الأعلى، بلا تخزين مؤقّت، تُعاد عند كلّ نقرة زرّ. النتيجة: تطبيق يستغرق ثلاث ثوانٍ لكلّ تفاعل. st.cache_data (الوحدة 5) يحلّ هذه المسألة بسطر واحد فوق دالّة القراءة، لكنّه يشترط منك أن تُنظّم قراءة البيانات داخل دوالّ صريحة، لا في المستوى الأعلى.

ثانيًا: كلّ ما يجب أن يبقى بين تفاعلَين يحتاج session_state. المتغيّرات المحلّيّة تختفي كلّ إعادة تنفيذ. إذا أردت عدّاد نقرات، أو خطوة نموذج، أو نتيجة تسجيل مستمرّة، لا يكفي count = 0 في الأعلى. st.session_state (الوحدة 6) هو القاموس الذي يبقى بين إعادات التنفيذ لجلسة المستخدم نفسها.

ثالثًا: ترتيب الشيفرة هو ترتيب الظهور. لا حاجة لـreturn، ولا لملفّ HTML مقابل. st.title قبل st.dataframe قبل st.plotly_chart يعطي بالتحديد هذا الترتيب على الصفحة. هذا يجعل الملفّ يُقرأ كنصّ خطّيّ، لكنّه يُلزم بترتيب الحسابات في مكان ظهورها، لا في مكان استعمالها.

الصفحة الأولى للخيط الأحمر

نبني الآن الصفحة الأولى لتطبيق ترك العملاء: عنوان، مقياس رئيسيّ، وجدول للعملاء ذوي الخطر. الشيفرة كاملة قابلة للتشغيل مباشرة:

import streamlit as st
import pandas as pd
import numpy as np

st.set_page_config(page_title="ترك العملاء", page_icon=":telephone:", layout="wide")

st.title("لوحة قيادة ترك العملاء")
st.caption("تحديث فوريّ لكلّ عميل يتغيّر وضعه.")

# بيانات تجريبيّة لعرض الصفحة قبل ربط النموذج الحقيقي في الوحدة 8.
rng = np.random.default_rng(42)
clients = pd.DataFrame({
"identifiant": [f"C{i:04d}" for i in range(200)],
"anciennete_mois": rng.integers(1, 72, 200),
"score_risque": rng.beta(2, 5, 200).round(3),
})
clients["a_risque"] = clients["score_risque"] > 0.6

col1, col2, col3 = st.columns(3)
col1.metric("عدد العملاء", len(clients))
col2.metric("عملاء بخطر", int(clients["a_risque"].sum()))
col3.metric("متوسّط الأقدميّة (شهر)", int(clients["anciennete_mois"].mean()))

st.subheader("العملاء ذوو الخطر")
st.dataframe(
clients[clients["a_risque"]].sort_values("score_risque", ascending=False),
hide_index=True,
use_container_width=True,
)

خمس نقاط في هذه الشيفرة تُلخّص الوحدة. set_page_config يجب أن يكون أوّل استدعاء لـStreamlit في السكربت؛ وإلّا رفع خطأ. st.title وst.caption عنصرا نصّ مكتفيان بذاتهما. st.columns(3) ينتج ثلاثة أعمدة يعرض كلّ منها مقياسًا واحدًا. st.dataframe يعرض جدولًا تفاعليًّا قابل الفرز بلا أيّ ضبط إضافيّ. توليد البيانات بـnp.random.default_rng(42) مع بذرة ثابتة يجعل التطبيق قابلًا للتكرار مؤقّتًا، إلى أن نربط النموذج الحقيقيّ في الوحدة 8.

الفخّ الأوّل: التصميم كأنّه Flask

كثير من المطوّرين القادمين من Flask أو Django يكتبون سطرًا مثل:

if st.button("احسب"):
resultat = fonction_lente() # يعمل مرّة واحدة عند الضغط
afficher(resultat)

النيّة أن «تعمل الدالّة مرّة واحدة عند النقر». الواقع: عند النقر يُعاد تنفيذ الملفّ. st.button يُعيد True مرّة واحدة (في التنفيذ الذي تلا النقر)، ثمّ يُعيد False بعده. فإذا كان القرار مبنيًّا على النقر وحده، resultat يختفي في التنفيذ التالي. الحلّ هو حفظ النتيجة في st.session_state مباشرة داخل كتلة if، ثمّ عرضها خارجها بناءً على وجودها. سنعود إليه في الوحدة 6 بالتفصيل.

اتّجاه القراءة من اليمين إلى اليسار

set_page_config(layout="wide") يمنح مساحة أوسع، لكنّه لا يُغيّر اتّجاه الكتابة. لعرض عربيّ لطيف، أضف في assets/rtl.css (سنراه في الوحدة 9) قاعدة body { direction: rtl; } واستعمل st.markdown(..., unsafe_allow_html=True) بحذر لحقنها. تصميم Streamlit افتراضيًّا لاتّجاه LTR، والعناصر الفرعيّة كالأعمدة تبقى مقروءة من اليسار إلى اليمين حتّى مع نصّ عربيّ.

الخلاصة

  • Streamlit يُعاد تنفيذ سكربته من أعلى إلى أسفل عند كلّ تفاعل؛ لا مستمعي أحداث، لا onChange.
  • st.set_page_config أوّل استدعاء، وst.title وst.metric وst.dataframe مكوّنات مكتفية بذاتها.
  • إعادة التنفيذ الشاملة تفرض ثلاث قواعد: تخزين ما يبطئ، حفظ ما يجب أن يبقى، وترتيب الكود ترتيب الظهور.
  • الفخّ الشائع: التصميم كأنّه Flask بشرط if st.button(...)؛ الحلّ يمرّ بـsession_state (الوحدة 6).

الوحدة التالية: مكوّنات الإدخال والعرض التي تسمح ببناء أوّل نموذج تسجيل حقيقيّ.