

Data Analysisتحليل البيانات
Use SQL, statistics, exploratory analysis and clear charts to turn data into evidence for decisions.
تستخدم SQL والإحصاء والتحليل الاستكشافي والرسوم الواضحة لتحويل البيانات إلى دليل يدعم القرار.
Units in this stageوحدات هذه المرحلة
- U11SQL Foundationsأساسيات SQL
4 lessons + exit check4 دروس + تقييم نهاية الوحدة - U12SQL for Real AnalyticsSQL للتحليل الحقيقي
5 lessons + exit check5 دروس + تقييم نهاية الوحدة - U13Statistics for Data Scienceالإحصاء لعلم البيانات
5 lessons + exit check5 دروس + تقييم نهاية الوحدة - U14Exploratory Data Analysisالتحليل الاستكشافي للبيانات
5 lessons + exit check5 دروس + تقييم نهاية الوحدة - U15Data Visualization & Storytellingتصوير البيانات وسرد القصة
4 lessons + exit check4 دروس + تقييم نهاية الوحدة - U16Data-Driven Business & Decision Makingاتخاذ القرار المبني على البيانات
6 lessons + exit check6 دروس + تقييم نهاية الوحدة
Have a pass? عندك اشتراك؟ Sign in to open every lessonسجّل الدخول لفتح كل الدروس.
U11 · SQL Foundationsأساسيات SQL
Stage 3المرحلة 3Free: the first lesson. The other lessons, the exit brief and the practice files are for Starter and Membership.
مجاني: الدرس الأول. باقي الدروس وتكليف نهاية الوحدة وملفات التمرين لأصحاب باقة البداية والاشتراك.
Download U11 practice files (.zip, 83 KB)حمّل ملفات تمرين U11 (zip، 83 KB)
Unzip into a short folder such as D:\DWorld on Windows. The zip has only learner files: no answers.
فك الضغط في مجلد قصير مثل D:\DWorld على ويندوز. الملف يحتوي ملفات المتعلم فقط بدون إجابات.
L43 · Relational Databases & Your First SELECTL43 · قواعد البيانات العلائقية وأول استعلام SELECT Freeمجاني
L43 — Relational Databases & Your First SELECT
Learner file. Answers and model solutions are in a separate instructor-only file. Version: U11 Production v2 · Content date: 19 October 2026. Time estimates are planning estimates until learners have tested them. The code in this lesson was run with the course setup from L21 (Python 3.13, pandas 3.0 and NumPy 2.5; SciPy 1.18 and Matplotlib 3.11 where used) on Linux and on Windows 11 on 4 November 2026 (see the Code Test Report). The queries behave the same on any SQLite 3.25 or later.
All businesses are fictional and all data is synthetic. Customers are pseudonymous IDs only.
1. Lesson Identity
| Field | Value |
|---|---|
| Title | Relational Databases & Your First SELECT |
| Stage | Stage 3 — Data Analysis |
| Unit | U11 — SQL Foundations |
| Lesson ID | L43 |
| Skills | Reading a database schema; connecting from Python; SELECT, aliases, calculated columns, ORDER BY, LIMIT, DISTINCT; environment check |
| Difficulty | Intermediate (first SQL lesson) |
| Estimated time | About 2.5 hours (planning estimate, unvalidated) |
| Assessment level | Practice |
| Prerequisites | L21 (notebooks), L25 (DataFrames and columns), L26 (sorting), L28 (keys and merges as an idea). |
Time breakdown (planning estimate):
- Environment check and first connection: 15 min.
- Concept and worked example: 55 min.
- Guided practice: 25 min.
- Independent practice: 25 min.
- Knowledge check: 10 min.
- AI in Practice and Verification: 5 min (optional).
- Workplace task and mini challenge: 15 min.
What you need: your L21 environment and practice_files (L43_starter.ipynb; in u11_data: cedar_lane.db and hilltop_honey.db). Nothing new to install: sqlite3 is part of Python, and SQLite needs no server, account or password.
2. Learning Objective
By the end of this lesson, you will be able to:
- explain tables, rows, columns, primary keys and foreign keys using the Cedar Lane database;
- connect to a SQLite database from a notebook and list its tables and columns;
- write
SELECTqueries with chosen columns, aliases, calculated columns,ORDER BY,LIMITandDISTINCT; - report the SQLite version your Python uses.
D.World approved objective (verbatim): Query a SQLite database from a notebook to list chosen columns with aliases, sorted and limited rows and distinct values, and report the SQLite version your Python uses.
3. Why It Matters
In Stage 2 every dataset arrived as a file: a CSV, a workbook, saved JSON pages. In most companies the data you need does not wait in a folder. It lives in a database, the system the shop, the website and the warehouse write to all day. Cedar Lane (fictional) records every order, every order line and every product there.
When the store manager asks "what are our most expensive products?" or "show me last week's online orders", there are two ways to answer:
- wait for someone to export a spreadsheet, then load it;
- ask the database directly, in SQL, and get the answer in seconds.
SQL (Structured Query Language) is the standard language for asking relational databases questions. It is one of the most requested skills in data job postings, and almost every analytics tool speaks it. In U11 you learn to question one table at a time; in U12 you combine tables.
Key principle: a query is only an answer when you know exactly which table and grain it reads, which columns and rows it returns, and in which order.
4. Concept
import sqlite3
import pandas as pd
con = sqlite3.connect("u11_data/cedar_lane.db")
def q(sql, params=()):
"""Run a SQL query on the Cedar Lane database and return a DataFrame."""
return pd.read_sql_query(sql, con, params=params)
print("SQLite library version:", sqlite3.sqlite_version)
SQLite library version: 3.45.1
Three things happened. sqlite3.connect opened the database file (SQLite keeps a whole database in one file; there is no server). The small helper q sends a SQL query and returns the result as a pandas DataFrame, using pd.read_sql_query. And sqlite3.sqlite_version printed the version of the SQLite library inside your Python. Window functions (L49) need 3.25 or later; the optional RIGHT and FULL joins need 3.39 or later.
Part A — Relational databases: tables, rows, columns, keys
A relational database stores data in tables. Each table holds one kind of thing; each row is one of those things; each column is one property of it. Tables are linked by shared values:
- a primary key is the column whose value identifies each row uniquely (
order_idinorders); - a foreign key is a column that holds another table's primary key (
orders.customer_idpoints tocustomers.customer_id).
This is the same idea as the keys you used with merge in L28, now built into the storage itself. The set of tables, columns, types and keys is the database's schema.
Part B — Look at the schema before you query
Every SQLite database has a built-in catalogue table, sqlite_master, that lists its tables and views:
print(q("SELECT type, name FROM sqlite_master WHERE type IN ('table', 'view') ORDER BY type, name").to_string(index=False))
type name
table campaign_summary
table customers
table order_items
table orders
table products
table regions
view order_lines_v
Six tables and one view. A view is a saved query that you can read like a table; it stores no data of its own. To see the columns of one table, use PRAGMA table_info:
cols = q("PRAGMA table_info(orders)")
print(cols[["name", "type", "notnull", "pk"]].to_string(index=False))
name type notnull pk
order_id TEXT 0 1
customer_id TEXT 1 0
region_code TEXT 1 0
created_at TEXT 1 0
order_date TEXT 1 0
status TEXT 1 0
promised_date TEXT 1 0
delivery_date TEXT 0 0
pk = 1 marks the primary key; notnull = 1 means the column may not be empty. delivery_date may be empty: an order that has not been delivered has no delivery date yet. In SQL an empty value is called NULL (L44 is about it).
SQLite typing is flexible. Most databases refuse to store text in a number column. SQLite usually accepts it, because the declared type is a preference (a "type affinity"), not a strict rule. typeof() shows what is actually stored in a cell:
print(q("SELECT order_id, typeof(order_id) AS t_id, order_date, typeof(order_date) AS t_date FROM orders LIMIT 2").to_string(index=False))
order_id t_id order_date t_date
O-1001 text 2026-07-01 text
O-1002 text 2026-07-02 text
Dates are stored as ISO text (YYYY-MM-DD). SQLite has no separate date type; date functions work on this text, and text in this format sorts in date order.
The view you will use most in U11. order_lines_v has one row per order line (one product in one order), with the order, customer segment, region and product details already attached. It means you can answer U11 questions from one place without joins; you will write the joins yourself in L47.
print(q("SELECT COUNT(*) AS lines FROM order_lines_v").to_string(index=False))
print(q("SELECT COUNT(*) AS orders FROM orders").to_string(index=False))
lines
511
orders
240
The view has more rows than orders because one order can have several lines. The grain (what one row means) is different: one row per order line versus one row per order. Always know which grain you are reading.
Part C — SELECT: columns, aliases and calculated columns
A query starts with SELECT (which columns) and FROM (which table):
print(q("SELECT product_id, product_name, list_price FROM products LIMIT 3").to_string(index=False))
product_id product_name list_price
P01 Rolled oats 1 kg 3.2
P02 Basmati rice 2 kg 6.5
P03 Red lentils 1 kg 4.1
- Choose columns by name.
SELECT *returns every column; useful for a first look, but in an answer name the columns you need, so the result does not change when someone adds a column to the table. - An alias renames a column in the result with
AS:list_price AS price. Aliases make results readable for the person who asked. - You can calculate in
SELECT:list_price - unit_cost AS margin.
print(q("""
SELECT product_name AS product,
list_price AS price,
unit_cost AS cost,
ROUND(list_price - unit_cost, 2) AS margin
FROM products
LIMIT 3
""").to_string(index=False))
product price cost margin
Rolled oats 1 kg 3.2 2.00 1.20
Basmati rice 2 kg 6.5 3.93 2.57
Red lentils 1 kg 4.1 2.66 1.44
ROUND(x, 2) rounds to two decimals. SQL keywords are not case-sensitive (select works too), but writing them in capitals and putting each clause on its own line makes queries easier to read and check.
Part D — ORDER BY and LIMIT
Without ORDER BY, SQL does not promise any row order. The order you see may change with the data, an index or the SQLite version. Whenever order matters ("top ten", "most recent"), say so:
ORDER BY list_price DESCsorts from highest to lowest (ASC, the default, is lowest first);ORDER BY category, list_price DESCsorts by category first, then by price inside each category;LIMIT 5keeps only the first five rows after sorting.
print(q("""
SELECT category, product_name, list_price
FROM products
ORDER BY category ASC, list_price DESC
LIMIT 5
""").to_string(index=False))
category product_name list_price
Drinks Ground coffee 500 g 12.9
Drinks Black tea 250 g 5.4
Drinks Apple juice 1 l 3.6
Drinks Oat drink 1 l 2.9
Fresh Apples 1 kg 2.8
A "top ten" query is always ORDER BY … DESC and LIMIT 10. LIMIT without ORDER BY returns ten rows, but not the ten you were asked for.
Part E — DISTINCT
DISTINCT removes duplicate rows from the result:
print(q("SELECT DISTINCT category FROM products ORDER BY category").to_string(index=False))
print(q("SELECT DISTINCT region_code, channel FROM order_lines_v ORDER BY region_code").to_string(index=False))
category
Drinks
Fresh
Household
Pantry
region_code channel
CEN store
HAR store
NOR store
STA store
WEB online
With two columns, DISTINCT keeps each distinct combination. It is a quick way to see which values a column contains before you filter on them (L44).
Part F — Written order versus logical order
You write clauses in this order: SELECT … FROM … ORDER BY … LIMIT …. The database processes them in a different order: first FROM (which table), then SELECT (which columns and calculations), then ORDER BY, then LIMIT. Two consequences you can use now:
ORDER BYcan use an alias defined inSELECT(it runs afterSELECT);LIMITacts on the final, sorted result.
When you add WHERE (L44) and GROUP BY/HAVING (L45), they slot into this order too; the Visual Explanation shows the full picture.
Part G — Check the result a second way
A query that runs without an error can still be wrong. Two quick checks you will use in every U11 lesson:
n_sql = q("SELECT COUNT(*) AS n FROM products")["n"].iloc[0]
products = q("SELECT * FROM products")
print(n_sql, len(products), n_sql == len(products))
12 12 True
COUNT(*) counts rows in SQL; len() counts rows in pandas. When the two agree, you know the whole table arrived. The second check is to read the result against the request: right columns, right sort direction, right number of rows.
5. Worked Example
Cedar Lane (fictional), synthetic data. The online manager writes: "Can you list the ten most recent orders, with the order number, when it was placed, the region and the status? Newest first, please, and use column names I can read."
Step 1 — Which table and grain? One row per order → orders, not the line view.
Step 2 — Which columns, with which names? order_id AS order_number, created_at AS placed_at, region_code AS region, status.
Step 3 — Which order and how many rows? Newest first → ORDER BY created_at DESC; ten → LIMIT 10. created_at holds date and time, so two orders on the same day are still ordered correctly.
recent = q("""
SELECT order_id AS order_number,
created_at AS placed_at,
region_code AS region,
status
FROM orders
ORDER BY created_at DESC
LIMIT 10
""")
print(recent.to_string(index=False))
order_number placed_at region status
O-1236 2026-10-14 17:03:43 WEB processing
O-1239 2026-10-14 15:35:07 WEB processing
O-1234 2026-10-14 15:11:20 STA delivered
O-1238 2026-10-14 15:03:13 HAR delivered
O-1235 2026-10-14 14:48:17 WEB delivered
O-1237 2026-10-14 14:03:10 CEN processing
O-1240 2026-10-14 13:20:17 CEN processing
O-1232 2026-10-13 18:55:44 NOR processing
O-1233 2026-10-13 18:11:24 HAR processing
O-1231 2026-10-12 20:34:15 WEB delivered
Step 4 — Check.
assert len(recent) == 10
assert recent["placed_at"].is_monotonic_decreasing
latest = q("SELECT MAX(created_at) AS latest FROM orders")["latest"].iloc[0]
print("newest in table:", latest, "| first row:", recent["placed_at"].iloc[0], "| match:", latest == recent["placed_at"].iloc[0])
newest in table: 2026-10-14 17:03:43 | first row: 2026-10-14 17:03:43 | match: True
MAX(created_at) finds the newest timestamp independently; it matches the first row, and the column is in decreasing order.
Step 5 — The note to the manager.
Here are the ten most recent online and store orders (newest first, placed between 12 and 14 October 2026). Several are still processing, which is expected for orders this recent. Query:
orders, sorted bycreated_atdescending, limited to 10.
6. Visual Explanation
HOW A QUERY IS WRITTEN HOW SQLITE PROCESSES IT
────────────────────────────── ──────────────────────────────────────────────
SELECT order_id AS order_number, ... ① FROM orders which table (and grain)?
FROM orders ② WHERE ... (L44) which rows?
WHERE ... (L44) ③ GROUP BY ... (L45) which groups?
GROUP BY ... (L45) ④ HAVING ... (L45) which groups to keep?
HAVING ... (L45) ⑤ SELECT ... AS ... which columns, calculations, aliases
ORDER BY created_at DESC ⑥ ORDER BY ... sorted how? (can use aliases)
LIMIT 10 ⑦ LIMIT 10 how many rows of the sorted result?
SCHEMA: customers ──(customer_id)──< orders ──(order_id)──< order_items >──(product_id)── products
regions ──(region_code)──< orders order_lines_v = one row per order line, details attached
Alt text: Two columns. On the left, the clauses of a query in the order they are written: SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, with WHERE marked as lesson 44 and GROUP BY and HAVING as lesson 45. On the right, the order in which SQLite processes them, numbered one to seven: FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT, each with the question it answers; ORDER BY can use aliases because it runs after SELECT. Below, the schema: customers and regions each link to many orders; each order links to many order items; each order item links to one product; the view order_lines_v has one row per order line with details attached.
7. Guided Practice
Open L43_starter.ipynb. Run the setup cell (it defines con and q as above).
- Print
sqlite3.sqlite_version. Is it at least 3.25? Write the answer in a Markdown cell. - List all tables and views from
sqlite_master. - Use
PRAGMA table_info(products)to list the columns ofproductsand their declared types. Which column is the primary key? - Select
product_id,product_nameandlist_pricefromproducts. Then add the aliaspriceforlist_price. - Add a calculated column
margin(price minus unit cost, rounded to 2 decimals). - Sort by
marginfrom highest to lowest, then show only the first five rows. - Check: count the rows of
productswithCOUNT(*)and withlen()on aSELECT *result. Do they match? - Run Restart Kernel and Run All Cells….
8. Independent Practice
An unseen database. Open u11_data/hilltop_honey.db (Hilltop Honey, fictional) with a second connection in L43_starter.ipynb (Part 2). You have not seen this database before.
- List its tables and, for each table, its columns and primary key.
- Write one sentence per table: what one row represents.
- Answer with one
SELECTeach: - Which towns does Hilltop Honey sell in, and on which market day? - Which jar sizes does it sell? (one row per size, smallest first) - What are the five individual sales with the most jars? Show the sale ID, date, market ID and jars, with readable column names. - For each answer, write how you checked it (a row count, a second query, or reading the result against the question).
9. Knowledge Check — Tables, Keys and SELECT
Q1. In the orders table, order_id is the primary key. What does that guarantee?
- A) Each order ID appears in exactly one row of
orders - B) Orders are stored in order-ID order
- C) Every order has at least one order line
- D) The column is a number
Q2. orders.customer_id holds values from customers.customer_id. What is orders.customer_id?
- A) A view
- B) An alias
- C) A foreign key
- D) A primary key
Q3. Why does order_lines_v have more rows than orders?
- A) Its grain is one row per order line, and one order can have several lines
- B) The view contains duplicate errors
- C) Views always double the rows
- D) It includes cancelled orders twice
Q4. What does SELECT list_price AS price FROM products change?
- A) It renames the column in the table permanently
- B) It converts prices to text
- C) It renames the column only in this result
- D) It sorts by price
Q5. You need the ten most expensive products. Which query is right?
- A)
SELECT DISTINCT list_price FROM products LIMIT 10 - B)
SELECT product_name, list_price FROM products LIMIT 10 - C)
SELECT product_name, list_price FROM products ORDER BY list_price LIMIT 10 - D)
SELECT product_name, list_price FROM products ORDER BY list_price DESC LIMIT 10
Q6. A query has no ORDER BY. What can you say about the row order?
- A) Rows come back in primary-key order
- B) Rows come back in the order they were inserted
- C) No order is guaranteed
- D) Rows come back sorted alphabetically
Q7. SELECT DISTINCT region_code, channel FROM order_lines_v returns what?
- A) Only the first region
- B) Every order line
- C) One row per distinct combination of region and channel
- D) The number of regions
Q8. Which Python expression shows the SQLite library version that sqlite3 uses?
- A)
sqlite3.sqlite_version - B)
con.version() - C)
pd.__version__ - D)
sqlite3.version
10. AI in Practice (optional, bounded)
Goal: ask an AI assistant to translate a plain-English request into a SELECT, then check its query against the request.
- Give it the column list of
products(fromPRAGMA table_info) and this request:
"Write a SQLite query that lists the five cheapest products with their category, name and price, with readable column names, cheapest first."
- Before running it, write down the columns, the sort direction and the number of rows you expect.
- Run its query and follow Section 11.
Boundaries:
- AI is optional.
- Share column names and table names only (they are synthetic here). Never paste real customer records into an AI tool.
- The AI cannot see your database. Column names it invents, or a missing
ORDER BY, are yours to catch.
11. AI Verification
- Columns: does the result have exactly the requested columns, with readable aliases?
- Sort direction: is it cheapest first (
ASC)? Check the first and last rows. - Row count: five rows? Did it use
ORDER BYbeforeLIMIT? - Cross-check: sort the full
productstable in pandas bylist_priceand compare the first five names. - Write down one thing the AI got right and one thing you had to fix (if any).
12. Business Lens
Quick questions deserve quick, correct answers. Store and online managers ask many small operational questions every day: the newest orders, the most expensive lines, which regions are active. When the analyst can answer straight from the database, decisions do not wait for a manual export, and everyone works from the same, current data.
The risk is that a quick answer is quietly wrong: the wrong table (lines instead of orders), no ORDER BY behind a "top five", a calculated column without units. The habit you start in L43 (name the table and grain, name the columns, state the order, check the count) is what makes a fast answer a trustworthy one.
13. Workplace Task — The Price List with Tax
Fictional business: Cedar Lane. The store manager writes:
"For the shelf-label review I need the ten most expensive products: product, category, price, and the price including tax. Use our standard 20% rate for the tax column. Clear column names, please."
- Write one query that answers the request (the tax column is calculated in
SELECT, rounded to 2 decimals). - Check it: ten rows; sorted from most expensive; one price-with-tax value recomputed by hand.
- Write a two-line note to the manager on what the result shows.
14. Mini Challenge — A Query That Looks Right
Fictional business: Cedar Lane. A colleague wrote this to get "the five products with the highest margin":
print(q("SELECT product_name, list_price - unit_cost AS margin FROM products LIMIT 5").to_string(index=False))
- Before running it, predict what is wrong.
- Run it and explain why the result does not answer the question.
- Fix the query and prove that your five rows really are the five highest margins.
- Write one sentence your colleague can keep as a rule.
Submit before feedback. No solution is provided here.
15. Resources
Required (about 30 min):
- Python docs:
sqlite3— DB-API 2.0 interface for SQLite databases, the tutorial at the top of the page (connect, execute, fetch) andsqlite3.sqlite_version. https://docs.python.org/3/library/sqlite3.html (Opened 19 Oct 2026.) - SQLite: SELECT, the overview of the simple
SELECTand theORDER BYandLIMITsections. https://www.sqlite.org/lang_select.html
Recommended:
- SQLite: Quirks, Caveats, and Gotchas In SQLite, sections on flexible typing and on dates stored as text. https://www.sqlite.org/quirks.html
- pandas API:
read_sql_query, how a SQL result becomes a DataFrame and howparams=passes values. https://pandas.pydata.org/docs/reference/api/pandas.read_sql_query.html
Reference (look things up when needed):
- SQLite home page, for the latest release and the documentation index. https://sqlite.org/
Open follow-up (D.World): the version of SQLite bundled with Python has not yet been install-tested on Windows, macOS and Linux for this course (SN03). L43 asks every learner to print and report it.
16. Person to Follow / Role Model
D. Richard Hipp (mapped by D.World for L43–L46).
- Verified fact: SQLite was originally developed and released in 2000 by Dr. D. Richard Hipp (SQLite Consortium press release, cited in the D.World Stage 3 map).
- Why D.World recommends him: the database you are querying, and the one inside Python, phones and browsers, comes from his project. The SQLite documentation you read in U11 is unusually clear about its own quirks.
- Honest note: we cite only the fact above.
- Try this: open the SQLite home page and find the "Quirks" page. Which quirk from this lesson does it describe first?
17. Glossary
| Term | Meaning in this lesson |
|---|---|
| Relational database | Data stored in tables that are linked by shared key values |
| Table | A set of rows of one kind of thing, with named columns |
| Primary key | The column whose value identifies each row uniquely, e.g. orders.order_id |
| Foreign key | A column holding another table's primary key, e.g. orders.customer_id |
| Schema | The tables, columns, types and keys of a database |
| Query | A request for data written in SQL |
| SQLite | A database engine that keeps a whole database in one file and is built into Python as sqlite3 |
| Alias | A new name for a column in a result, written with AS |
| DISTINCT | Keeps only distinct rows (or combinations of values) in a result |
| Result set | The rows and columns a query returns |
Review: DataFrame and columns (L25) · sorting (L26) · notebooks (L21).
18. Reflection
- Explain to a colleague, without SQL, why "top ten" needs both a sort and a limit.
- Which grain would you use to count orders, and which to count products sold? Why?
- What did you find surprising about how SQLite stores dates?
19. Next Step
You can now read a schema and write sorted, limited, readable SELECT queries. Next:
- L44 — Filtering Rows: WHERE, Logic & NULL. Questions rarely want every row. You will return exactly the rows a request describes, handle missing values correctly and pass values safely from Python.
20. Assessment Evidence
| Evidence | What it shows | How it is judged |
|---|---|---|
| Environment check + unseen-database queries (Independent Practice) | Reads an unfamiliar schema; states the grain; answers "show me" questions with correct columns, sort and limit; checks each answer | Rubric L43-IP |
| Guided practice (products) | Schema inspection, aliases, calculated column, sort and limit, count check | Rubric L43-GP |
| Knowledge check (Q1–Q8) | Keys, grain, aliases, ORDER BY with LIMIT, DISTINCT, version | 7 or more of 8 correct |
| Workplace task and mini challenge | Query file with the price-with-tax query and note; finds a missing ORDER BY | Rubrics L43-WT, L43-MC |
References
- Python Software Foundation. sqlite3 — DB-API 2.0 interface for SQLite databases. https://docs.python.org/3/library/sqlite3.html (PY-SQLITE). Supports: connecting,
sqlite3.sqlite_version. - SQLite. SELECT. https://www.sqlite.org/lang_select.html (SQ-SEL). Supports: SELECT, aliases, ORDER BY, LIMIT, DISTINCT; no guaranteed order without ORDER BY.
- SQLite. Quirks, Caveats, and Gotchas In SQLite. https://www.sqlite.org/quirks.html (SQ-QRK). Supports: flexible typing, dates as text.
- pandas development team. pandas.read_sql_query. https://pandas.pydata.org/docs/reference/api/pandas.read_sql_query.html (P-RSQL). Supports: SQL results as DataFrames,
params=. - SQLite. Home page. https://sqlite.org/ (SQ-HOME). Supports: release information.
- D.World Stage 3 Detailed Lesson Map v0.9 (L43 row; role model fact; SN03; U11 uses the provided view, joins start in L47). Internal.
L43 — قواعد البيانات العلائقية وأول استعلام SELECT
ملف المتعلّم. الإجابات والحلول النموذجية في ملف منفصل خاص بالمدرّس فقط. الإصدار: U11 Production v2 · تاريخ المحتوى: 19 أكتوبر 2026. تقديرات الوقت تقديرات للتخطيط إلى أن يختبرها المتعلّمون. الكود في هذا الدرس شُغّل باستخدام تجهيزات الدورة من L21 (Python 3.13 وpandas 3.0 وNumPy 2.5؛ وSciPy 1.18 وMatplotlib 3.11 حيث استُخدمتا) على Linux وعلى Windows 11 في 4 نوفمبر 2026 (راجع تقرير اختبار الكود Code Test Report). وتعمل الاستعلامات بالطريقة نفسها على أي إصدار SQLite من 3.25 فما فوق.
كل الأنشطة التجارية خيالية، وكل البيانات مصطنعة. والعملاء ممثَّلون بأرقام تعريف مستعارة فقط.
1. بطاقة الدرس
| البند | القيمة |
|---|---|
| العنوان | قواعد البيانات العلائقية وأول استعلام SELECT |
| المرحلة | المرحلة 3 — تحليل البيانات |
| الوحدة | U11 — أساسيات SQL |
| رقم الدرس | L43 |
| المهارات | قراءة مخطط قاعدة البيانات (schema)؛ الاتصال من Python؛ SELECT والأسماء البديلة (aliases) والأعمدة المحسوبة وORDER BY وLIMIT وDISTINCT؛ فحص البيئة |
| المستوى | متوسط (أول درس في SQL) |
| الوقت المتوقع | حوالي ساعتين ونصف (تقدير للتخطيط، غير مُتحقَّق منه) |
| مستوى التقييم | تدريب |
| المتطلبات السابقة | L21 (الدفاتر)، وL25 (DataFrame والأعمدة)، وL26 (الترتيب)، وL28 (المفاتيح والدمج كفكرة). |
تفصيل الوقت (تقدير للتخطيط):
- فحص البيئة وأول اتصال: 15 دقيقة.
- المفهوم والمثال المحلول: 55 دقيقة.
- التدريب الموجَّه: 25 دقيقة.
- التدريب المستقل: 25 دقيقة.
- اختبار الفهم: 10 دقائق.
- الذكاء الاصطناعي في التطبيق والتحقق منه: 5 دقائق (اختياري).
- مهمة العمل والتحدي القصير: 15 دقيقة.
ما تحتاج إليه: بيئتك من L21 ومجلد practice_files (L43_starter.ipynb؛ وفي u11_data: cedar_lane.db وhilltop_honey.db). لا شيء جديد للتثبيت: فالوحدة sqlite3 جزء من Python، وSQLite لا تحتاج إلى خادم ولا حساب ولا كلمة مرور.
2. هدف التعلّم
في نهاية هذا الدرس ستكون قادرًا على أن:
- تشرح الجداول والصفوف والأعمدة والمفاتيح الأساسية والمفاتيح الأجنبية باستخدام قاعدة بيانات Cedar Lane؛
- تتصل بقاعدة بيانات SQLite من دفتر وتسرد جداولها وأعمدتها؛
- تكتب استعلامات
SELECTبأعمدة مختارة وأسماء بديلة وأعمدة محسوبة وORDER BYوLIMITوDISTINCT؛ - تذكر إصدار SQLite الذي يستخدمه Python عندك.
الهدف المعتمد من D.World (بنصّه): Query a SQLite database from a notebook to list chosen columns with aliases, sorted and limited rows and distinct values, and report the SQLite version your Python uses.
3. لماذا يهمّك هذا؟
في المرحلة 2 وصلت كل مجموعة بيانات على هيئة ملف: CSV، أو مصنّف، أو صفحات JSON محفوظة. أما في معظم الشركات فالبيانات التي تحتاج إليها لا تنتظرك في مجلد. إنها تعيش في قاعدة بيانات (database)، وهي النظام الذي يكتب فيه المتجر والموقع والمستودع طوال اليوم. وتسجّل Cedar Lane (نشاط خيالي) هناك كل طلب، وكل سطر طلب، وكل منتج.
عندما يسأل مدير المتجر "ما أغلى منتجاتنا؟" أو "اعرض لي طلبات الأسبوع الماضي الإلكترونية"، توجد طريقتان للإجابة:
- أن تنتظر حتى يصدّر أحدهم جدول بيانات، ثم تحمّله؛
- أن تسأل قاعدة البيانات مباشرة، بلغة SQL، وتحصل على الإجابة في ثوانٍ.
لغة SQL (لغة الاستعلام المنظَّمة Structured Query Language) هي اللغة المعيارية لطرح الأسئلة على قواعد البيانات العلائقية. وهي من أكثر المهارات المطلوبة في إعلانات وظائف البيانات، وتكاد كل أدوات التحليل تتحدث بها. في U11 تتعلّم استجواب جدول واحد في كل مرة؛ وفي U12 تجمع بين الجداول.
المبدأ الأساسي: الاستعلام لا يكون إجابة إلا عندما تعرف بالضبط أي جدول وأي مستوى تفصيل يقرأ، وأي أعمدة وصفوف يُرجع، وبأي ترتيب.
4. المفهوم
import sqlite3
import pandas as pd
con = sqlite3.connect("u11_data/cedar_lane.db")
def q(sql, params=()):
"""Run a SQL query on the Cedar Lane database and return a DataFrame."""
return pd.read_sql_query(sql, con, params=params)
print("SQLite library version:", sqlite3.sqlite_version)
(نص التوثيق داخل الدالة: شغّل استعلام SQL على قاعدة بيانات Cedar Lane وأرجِع DataFrame.)
SQLite library version: 3.45.1
حدثت ثلاثة أشياء. فتحت sqlite3.connect ملف قاعدة البيانات (تحفظ SQLite قاعدة البيانات كلها في ملف واحد؛ ولا يوجد خادم). والدالة المساعدة الصغيرة q ترسل استعلام SQL وتُرجع النتيجة على هيئة DataFrame في pandas، باستخدام pd.read_sql_query. وطبعت sqlite3.sqlite_version إصدار مكتبة SQLite الموجودة داخل Python عندك. ودوال النوافذ (window functions) (L49) تحتاج إلى 3.25 أو أحدث؛ وعمليات الربط الاختيارية RIGHT وFULL تحتاج إلى 3.39 أو أحدث.
الجزء A — قواعد البيانات العلائقية: الجداول والصفوف والأعمدة والمفاتيح
قاعدة البيانات العلائقية (relational database) تخزّن البيانات في جداول (tables). كل جدول يحمل نوعًا واحدًا من الأشياء؛ وكل صف واحد من تلك الأشياء؛ وكل عمود خاصية واحدة منه. وترتبط الجداول بقيم مشتركة:
- المفتاح الأساسي (primary key) هو العمود الذي تميّز قيمته كل صف تمييزًا فريدًا (
order_idفيorders)؛ - المفتاح الأجنبي (foreign key) عمود يحمل المفتاح الأساسي لجدول آخر (
orders.customer_idيشير إلىcustomers.customer_id).
هذه هي فكرة المفاتيح نفسها التي استخدمتها مع merge في L28، لكنها الآن مبنية في نظام التخزين نفسه. ومجموعة الجداول والأعمدة والأنواع والمفاتيح هي مخطط (schema) قاعدة البيانات.
الجزء B — انظر إلى المخطط قبل أن تستعلم
في كل قاعدة بيانات SQLite جدول فهرسة مدمج اسمه sqlite_master، يسرد جداولها وطرق العرض (views) فيها:
print(q("SELECT type, name FROM sqlite_master WHERE type IN ('table', 'view') ORDER BY type, name").to_string(index=False))
type name
table campaign_summary
table customers
table order_items
table orders
table products
table regions
view order_lines_v
ستة جداول وطريقة عرض (view) واحدة. وطريقة العرض استعلام محفوظ تستطيع قراءته كأنه جدول؛ ولا تخزّن أي بيانات خاصة بها. ولرؤية أعمدة جدول واحد، استخدم PRAGMA table_info:
cols = q("PRAGMA table_info(orders)")
print(cols[["name", "type", "notnull", "pk"]].to_string(index=False))
name type notnull pk
order_id TEXT 0 1
customer_id TEXT 1 0
region_code TEXT 1 0
created_at TEXT 1 0
order_date TEXT 1 0
status TEXT 1 0
promised_date TEXT 1 0
delivery_date TEXT 0 0
pk = 1 يعلّم المفتاح الأساسي؛ وnotnull = 1 يعني أن العمود لا يجوز أن يكون فارغًا. أما delivery_date فيجوز أن يكون فارغًا: فالطلب الذي لم يُسلَّم بعد ليس له تاريخ توصيل. وفي SQL تُسمّى القيمة الفارغة NULL (وموضوعها L44).
الأنواع في SQLite مرنة. معظم قواعد البيانات ترفض تخزين نص في عمود رقمي. أما SQLite فتقبله عادةً، لأن النوع المُعلَن تفضيل ("ميل نوعي" type affinity)، لا قاعدة صارمة. والدالة typeof() تُظهر ما هو مخزَّن فعلًا في الخلية:
print(q("SELECT order_id, typeof(order_id) AS t_id, order_date, typeof(order_date) AS t_date FROM orders LIMIT 2").to_string(index=False))
order_id t_id order_date t_date
O-1001 text 2026-07-01 text
O-1002 text 2026-07-02 text
التواريخ مخزَّنة كنص بصيغة ISO (YYYY-MM-DD). وليس في SQLite نوع مستقل للتاريخ؛ فدوال التاريخ تعمل على هذا النص، والنص بهذه الصيغة يُرتَّب بترتيب التواريخ.
طريقة العرض التي ستستخدمها أكثر من غيرها في U11. في order_lines_v صف واحد لكل سطر طلب (منتج واحد في طلب واحد)، وتفاصيل الطلب وشريحة العميل والمنطقة والمنتج مرفقة به بالفعل. وهذا يعني أنك تستطيع الإجابة عن أسئلة U11 من مكان واحد دون عمليات ربط (joins)؛ وستكتب عمليات الربط بنفسك في L47.
print(q("SELECT COUNT(*) AS lines FROM order_lines_v").to_string(index=False))
print(q("SELECT COUNT(*) AS orders FROM orders").to_string(index=False))
lines
511
orders
240
في طريقة العرض صفوف أكثر مما في orders لأن الطلب الواحد قد يضم عدة أسطر. فـمستوى التفصيل (grain) (ما يعنيه الصف الواحد) مختلف: صف لكل سطر طلب مقابل صف لكل طلب. واعرف دائمًا أي مستوى تفصيل تقرأ.
الجزء C — SELECT: الأعمدة والأسماء البديلة والأعمدة المحسوبة
يبدأ الاستعلام بـ SELECT (أي الأعمدة) وFROM (أي الجداول):
print(q("SELECT product_id, product_name, list_price FROM products LIMIT 3").to_string(index=False))
product_id product_name list_price
P01 Rolled oats 1 kg 3.2
P02 Basmati rice 2 kg 6.5
P03 Red lentils 1 kg 4.1
- اختر الأعمدة بأسمائها. الأمر
SELECT *يُرجع كل الأعمدة؛ وهو مفيد لنظرة أولى، لكن في الإجابة سمِّ الأعمدة التي تحتاج إليها، حتى لا تتغير النتيجة عندما يضيف أحدهم عمودًا إلى الجدول. - الاسم البديل (alias) يعيد تسمية عمود في النتيجة باستخدام
AS:list_price AS price. والأسماء البديلة تجعل النتائج مقروءة لمن طرح السؤال. - يمكنك أن تحسب داخل
SELECT:list_price - unit_cost AS margin.
print(q("""
SELECT product_name AS product,
list_price AS price,
unit_cost AS cost,
ROUND(list_price - unit_cost, 2) AS margin
FROM products
LIMIT 3
""").to_string(index=False))
product price cost margin
Rolled oats 1 kg 3.2 2.00 1.20
Basmati rice 2 kg 6.5 3.93 2.57
Red lentils 1 kg 4.1 2.66 1.44
الدالة ROUND(x, 2) تقرّب إلى منزلتين عشريتين. والكلمات المفتاحية في SQL لا تتأثر بحالة الأحرف (select تعمل أيضًا)، لكن كتابتها بأحرف كبيرة ووضع كل جزء من الاستعلام (clause) في سطر خاص يجعلان الاستعلامات أسهل في القراءة والمراجعة.
الجزء D — ORDER BY وLIMIT
من دون ORDER BY لا تَعِد SQL بأي ترتيب للصفوف. فالترتيب الذي تراه قد يتغير مع البيانات، أو مع فهرس (index)، أو مع إصدار SQLite. وكلما كان الترتيب مهمًا ("أعلى عشرة"، "الأحدث")، فصرّح به:
ORDER BY list_price DESCيرتّب من الأعلى إلى الأدنى (وASC، وهو الافتراضي، يبدأ بالأدنى)؛ORDER BY category, list_price DESCيرتّب حسب الفئة أولًا، ثم حسب السعر داخل كل فئة؛LIMIT 5لا يُبقي إلا أول خمسة صفوف بعد الترتيب.
print(q("""
SELECT category, product_name, list_price
FROM products
ORDER BY category ASC, list_price DESC
LIMIT 5
""").to_string(index=False))
category product_name list_price
Drinks Ground coffee 500 g 12.9
Drinks Black tea 250 g 5.4
Drinks Apple juice 1 l 3.6
Drinks Oat drink 1 l 2.9
Fresh Apples 1 kg 2.8
استعلام "أعلى عشرة" هو دائمًا ORDER BY … DESC وLIMIT 10. أما LIMIT من دون ORDER BY فيُرجع عشرة صفوف، لكنها ليست العشرة التي طُلبت منك.
الجزء E — DISTINCT
الكلمة DISTINCT تزيل الصفوف المكرّرة من النتيجة:
print(q("SELECT DISTINCT category FROM products ORDER BY category").to_string(index=False))
print(q("SELECT DISTINCT region_code, channel FROM order_lines_v ORDER BY region_code").to_string(index=False))
category
Drinks
Fresh
Household
Pantry
region_code channel
CEN store
HAR store
NOR store
STA store
WEB online
مع عمودين، تُبقي DISTINCT كل تركيبة مختلفة. وهي طريقة سريعة لترى القيم التي يحتوي عليها عمود قبل أن تصفّي بها (L44).
الجزء F — ترتيب الكتابة مقابل الترتيب المنطقي
أنت تكتب أجزاء الاستعلام بهذا الترتيب: SELECT … FROM … ORDER BY … LIMIT …. أما قاعدة البيانات فتعالجها بترتيب مختلف: أولًا FROM (أي جدول)، ثم SELECT (أي أعمدة وحسابات)، ثم ORDER BY، ثم LIMIT. ونتيجتان تستطيع الاستفادة منهما الآن:
- يستطيع
ORDER BYاستخدام اسم بديل عُرّف فيSELECT(لأنه يعمل بعدSELECT)؛ - يعمل
LIMITعلى النتيجة النهائية المرتّبة.
وعندما تضيف WHERE (L44) وGROUP BY/HAVING (L45)، فإنها تأخذ مكانها في هذا الترتيب أيضًا؛ ويعرض الشرح البصري الصورة كاملة.
الجزء G — تحقّق من النتيجة بطريقة ثانية
الاستعلام الذي يعمل بلا خطأ قد يكون خاطئًا مع ذلك. وفحصان سريعان ستستخدمهما في كل دروس U11:
n_sql = q("SELECT COUNT(*) AS n FROM products")["n"].iloc[0]
products = q("SELECT * FROM products")
print(n_sql, len(products), n_sql == len(products))
12 12 True
الدالة COUNT(*) تعدّ الصفوف في SQL؛ والدالة len() تعدّ الصفوف في pandas. وعندما يتفق الاثنان، تعرف أن الجدول وصل كاملًا. والفحص الثاني أن تقرأ النتيجة مقابل الطلب: الأعمدة الصحيحة، واتجاه الترتيب الصحيح، والعدد الصحيح من الصفوف.
5. مثال محلول
Cedar Lane (نشاط خيالي)، بيانات مصطنعة. يكتب مدير المتجر الإلكتروني: "Can you list the ten most recent orders, with the order number, when it was placed, the region and the status? Newest first, please, and use column names I can read." (أي: "هل يمكنك سرد أحدث عشرة طلبات، مع رقم الطلب، ووقت تقديمه، والمنطقة، والحالة؟ الأحدث أولًا من فضلك، واستخدم أسماء أعمدة أستطيع قراءتها.")
الخطوة 1 — أي جدول وأي مستوى تفصيل؟ صف لكل طلب ← orders، لا طريقة عرض الأسطر.
الخطوة 2 — أي أعمدة، وبأي أسماء؟ order_id AS order_number وcreated_at AS placed_at وregion_code AS region وstatus.
الخطوة 3 — أي ترتيب وكم صفًا؟ الأحدث أولًا ← ORDER BY created_at DESC؛ وعشرة ← LIMIT 10. والعمود created_at يحمل التاريخ والوقت، فيبقى ترتيب طلبين في اليوم نفسه صحيحًا.
recent = q("""
SELECT order_id AS order_number,
created_at AS placed_at,
region_code AS region,
status
FROM orders
ORDER BY created_at DESC
LIMIT 10
""")
print(recent.to_string(index=False))
order_number placed_at region status
O-1236 2026-10-14 17:03:43 WEB processing
O-1239 2026-10-14 15:35:07 WEB processing
O-1234 2026-10-14 15:11:20 STA delivered
O-1238 2026-10-14 15:03:13 HAR delivered
O-1235 2026-10-14 14:48:17 WEB delivered
O-1237 2026-10-14 14:03:10 CEN processing
O-1240 2026-10-14 13:20:17 CEN processing
O-1232 2026-10-13 18:55:44 NOR processing
O-1233 2026-10-13 18:11:24 HAR processing
O-1231 2026-10-12 20:34:15 WEB delivered
الخطوة 4 — تحقّق.
assert len(recent) == 10
assert recent["placed_at"].is_monotonic_decreasing
latest = q("SELECT MAX(created_at) AS latest FROM orders")["latest"].iloc[0]
print("newest in table:", latest, "| first row:", recent["placed_at"].iloc[0], "| match:", latest == recent["placed_at"].iloc[0])
newest in table: 2026-10-14 17:03:43 | first row: 2026-10-14 17:03:43 | match: True
الدالة MAX(created_at) تجد أحدث طابع زمني بشكل مستقل؛ وهو يطابق الصف الأول، والعمود مرتّب تنازليًا.
الخطوة 5 — المذكرة إلى المدير.
إليك أحدث عشرة طلبات إلكترونية وفي المتاجر (الأحدث أولًا، قُدّمت بين 12 و14 أكتوبر 2026). عدد منها ما زال processing (قيد التجهيز)، وهذا متوقع لطلبات بهذه الحداثة. الاستعلام:
orders، مرتّب حسبcreated_atتنازليًا، ومحدود بعشرة صفوف.
6. شرح بصري
HOW A QUERY IS WRITTEN HOW SQLITE PROCESSES IT
────────────────────────────── ──────────────────────────────────────────────
SELECT order_id AS order_number, ... ① FROM orders which table (and grain)?
FROM orders ② WHERE ... (L44) which rows?
WHERE ... (L44) ③ GROUP BY ... (L45) which groups?
GROUP BY ... (L45) ④ HAVING ... (L45) which groups to keep?
HAVING ... (L45) ⑤ SELECT ... AS ... which columns, calculations, aliases
ORDER BY created_at DESC ⑥ ORDER BY ... sorted how? (can use aliases)
LIMIT 10 ⑦ LIMIT 10 how many rows of the sorted result?
SCHEMA: customers ──(customer_id)──< orders ──(order_id)──< order_items >──(product_id)── products
regions ──(region_code)──< orders order_lines_v = one row per order line, details attached
وصف الرسم: عمودان. على اليسار أجزاء الاستعلام بالترتيب الذي تُكتب به: SELECT وFROM وWHERE وGROUP BY وHAVING وORDER BY وLIMIT، مع الإشارة إلى أن WHERE موضوع الدرس 44، وGROUP BY وHAVING موضوع الدرس 45. وعلى اليمين الترتيب الذي تعالجها به SQLite، مرقّمًا من واحد إلى سبعة: FROM وWHERE وGROUP BY وHAVING وSELECT وORDER BY وLIMIT، ومع كل منها السؤال الذي يجيب عنه؛ ويستطيع ORDER BY استخدام الأسماء البديلة لأنه يعمل بعد SELECT. وفي الأسفل المخطط: يرتبط كل من customers وregions بطلبات كثيرة؛ وكل طلب يرتبط بعناصر طلب كثيرة؛ وكل عنصر طلب يرتبط بمنتج واحد؛ وطريقة العرض order_lines_v فيها صف لكل سطر طلب مع التفاصيل مرفقة. (نصوص الرسم: HOW A QUERY IS WRITTEN = كيف يُكتب الاستعلام؛ HOW SQLITE PROCESSES IT = كيف تعالجه SQLite؛ which table (and grain)? = أي جدول (وأي مستوى تفصيل)؟؛ which rows? = أي صفوف؟؛ which groups? = أي مجموعات؟؛ which groups to keep? = أي مجموعات نُبقي؟؛ which columns, calculations, aliases = أي أعمدة وحسابات وأسماء بديلة؛ sorted how? = مرتّب كيف؟؛ how many rows of the sorted result? = كم صفًا من النتيجة المرتّبة؟)
7. تدريب موجَّه
افتح L43_starter.ipynb. وشغّل خلية التجهيز (فهي تعرّف con وq كما في الأعلى).
- اطبع
sqlite3.sqlite_version. هل هو 3.25 على الأقل؟ اكتب الإجابة في خلية Markdown. - اسرد كل الجداول وطرق العرض من
sqlite_master. - استخدم
PRAGMA table_info(products)لسرد أعمدةproductsوأنواعها المُعلَنة. أي عمود هو المفتاح الأساسي؟ - اختر
product_idوproduct_nameوlist_priceمنproducts. ثم أضف الاسم البديلpriceلـlist_price. - أضف عمودًا محسوبًا
margin(السعر ناقص تكلفة الوحدة، مقرّبًا إلى منزلتين عشريتين). - رتّب حسب
marginمن الأعلى إلى الأدنى، ثم اعرض أول خمسة صفوف فقط. - تحقّق: عُدّ صفوف
productsبـCOUNT(*)وبـlen()على نتيجةSELECT *. هل يتطابقان؟ - شغّل Restart Kernel and Run All Cells….
8. تدريب مستقل
قاعدة بيانات لم ترها من قبل. افتح u11_data/hilltop_honey.db (Hilltop Honey، نشاط خيالي) باتصال ثانٍ في L43_starter.ipynb (الجزء 2). لم ترَ قاعدة البيانات هذه من قبل.
- اسرد جداولها، ولكل جدول أعمدته ومفتاحه الأساسي.
- اكتب جملة واحدة لكل جدول: ما الذي يمثّله الصف الواحد.
- أجب باستعلام
SELECTواحد لكل سؤال:- في أي البلدات تبيع Hilltop Honey، وفي أي يوم سوق؟
- ما أحجام البرطمانات التي تبيعها؟ (صف لكل حجم، الأصغر أولًا)
- ما عمليات البيع الفردية الخمس التي فيها أكبر عدد من البرطمانات؟ اعرض رقم البيع، والتاريخ، ورقم السوق، والبرطمانات، بأسماء أعمدة مقروءة.
- لكل إجابة، اكتب كيف تحققت منها (عدد صفوف، أو استعلام ثانٍ، أو قراءة النتيجة مقابل السؤال).
9. اختبار الفهم (Knowledge Check) — الجداول والمفاتيح وSELECT
Q1. في الجدول orders، العمود order_id هو المفتاح الأساسي. ماذا يضمن ذلك؟
- A) كل رقم طلب يظهر في صف واحد بالضبط من
orders - B) الطلبات مخزَّنة بترتيب أرقامها
- C) لكل طلب سطر طلب واحد على الأقل
- D) العمود رقمي
Q2. العمود orders.customer_id يحمل قيمًا من customers.customer_id. ما هو orders.customer_id؟
- A) طريقة عرض
- B) اسم بديل
- C) مفتاح أجنبي
- D) مفتاح أساسي
Q3. لماذا في order_lines_v صفوف أكثر مما في orders؟
- A) مستوى تفصيلها صف لكل سطر طلب، والطلب الواحد قد يضم عدة أسطر
- B) طريقة العرض فيها أخطاء تكرار
- C) طرق العرض تضاعف الصفوف دائمًا
- D) تتضمن الطلبات الملغاة مرتين
Q4. ماذا يغيّر الاستعلام SELECT list_price AS price FROM products؟
- A) يعيد تسمية العمود في الجدول بشكل دائم
- B) يحوّل الأسعار إلى نص
- C) يعيد تسمية العمود في هذه النتيجة فقط
- D) يرتّب حسب السعر
Q5. تحتاج إلى أغلى عشرة منتجات. أي استعلام صحيح؟
- A)
SELECT DISTINCT list_price FROM products LIMIT 10 - B)
SELECT product_name, list_price FROM products LIMIT 10 - C)
SELECT product_name, list_price FROM products ORDER BY list_price LIMIT 10 - D)
SELECT product_name, list_price FROM products ORDER BY list_price DESC LIMIT 10
Q6. استعلام ليس فيه ORDER BY. ماذا تستطيع أن تقول عن ترتيب الصفوف؟
- A) تعود الصفوف بترتيب المفتاح الأساسي
- B) تعود الصفوف بالترتيب الذي أُدخلت به
- C) لا يوجد ترتيب مضمون
- D) تعود الصفوف مرتّبة أبجديًا
Q7. ماذا يُرجع SELECT DISTINCT region_code, channel FROM order_lines_v؟
- A) المنطقة الأولى فقط
- B) كل سطر طلب
- C) صفًا واحدًا لكل تركيبة مختلفة من المنطقة والقناة
- D) عدد المناطق
Q8. أي تعبير في Python يُظهر إصدار مكتبة SQLite التي تستخدمها sqlite3؟
- A)
sqlite3.sqlite_version - B)
con.version() - C)
pd.__version__ - D)
sqlite3.version
10. الذكاء الاصطناعي في التطبيق (اختياري، وفي حدود)
الهدف: أن تطلب من مساعد ذكاء اصطناعي ترجمة طلب مكتوب بالإنجليزية البسيطة إلى استعلام SELECT، ثم تتحقق من استعلامه مقابل الطلب.
- أعطه قائمة أعمدة
products(منPRAGMA table_info) وهذا الطلب:
"Write a SQLite query that lists the five cheapest products with their category, name and price, with readable column names, cheapest first."
وبالعربية: "اكتب استعلام SQLite يسرد أرخص خمسة منتجات مع فئتها واسمها وسعرها، بأسماء أعمدة مقروءة، الأرخص أولًا."
- قبل تشغيله، دوّن الأعمدة واتجاه الترتيب وعدد الصفوف التي تتوقعها.
- شغّل استعلامه واتّبع القسم 11.
الحدود:
- استخدام الذكاء الاصطناعي اختياري.
- شارك أسماء الأعمدة وأسماء الجداول فقط (وهي مصطنعة هنا). ولا تلصق أبدًا سجلات عملاء حقيقيين في أداة ذكاء اصطناعي.
- الذكاء الاصطناعي لا يرى قاعدة بياناتك. وأسماء الأعمدة التي يخترعها، أو غياب
ORDER BY، مسؤوليتك أنت أن تكتشفها.
11. التحقق من مخرجات الذكاء الاصطناعي
- الأعمدة: هل في النتيجة الأعمدة المطلوبة بالضبط، بأسماء بديلة مقروءة؟
- اتجاه الترتيب: هل الأرخص أولًا (
ASC)؟ تحقّق من الصف الأول والأخير. - عدد الصفوف: خمسة صفوف؟ وهل استخدم
ORDER BYقبلLIMIT؟ - تحقّق متقاطع: رتّب جدول
productsكاملًا في pandas حسبlist_priceوقارن أول خمسة أسماء. - دوّن شيئًا واحدًا أصاب فيه الذكاء الاصطناعي، وشيئًا واحدًا اضطررت إلى إصلاحه (إن وُجد).
12. منظور الأعمال
الأسئلة السريعة تستحق إجابات سريعة وصحيحة. مديرو المتاجر والمتجر الإلكتروني يطرحون كل يوم أسئلة تشغيلية صغيرة كثيرة: أحدث الطلبات، وأغلى الأسطر، والمناطق النشطة. وعندما يستطيع المحلل الإجابة مباشرة من قاعدة البيانات، لا تنتظر القرارات تصديرًا يدويًا، ويعمل الجميع على البيانات نفسها والمحدّثة.
والخطر أن تكون الإجابة السريعة خاطئة بهدوء: الجدول الخطأ (الأسطر بدلًا من الطلبات)، أو غياب ORDER BY خلف "أعلى خمسة"، أو عمود محسوب بلا وحدات. والعادة التي تبدأها في L43 (سمِّ الجدول ومستوى التفصيل، وسمِّ الأعمدة، وصرّح بالترتيب، وتحقّق من العدد) هي ما يجعل الإجابة السريعة جديرة بالثقة.
13. مهمة عمل — قائمة الأسعار مع الضريبة
نشاط خيالي: Cedar Lane. يكتب مدير المتجر:
"For the shelf-label review I need the ten most expensive products: product, category, price, and the price including tax. Use our standard 20% rate for the tax column. Clear column names, please."
وبالعربية: "لمراجعة ملصقات الرفوف أحتاج إلى أغلى عشرة منتجات: المنتج، والفئة، والسعر، والسعر شاملًا الضريبة. استخدم نسبتنا المعتادة 20% لعمود الضريبة. وأسماء أعمدة واضحة من فضلك."
- اكتب استعلامًا واحدًا يجيب عن الطلب (عمود الضريبة يُحسب داخل
SELECT، مقرّبًا إلى منزلتين عشريتين). - تحقّق منه: عشرة صفوف؛ مرتّبة من الأغلى؛ وقيمة واحدة للسعر مع الضريبة أعدت حسابها يدويًا.
- اكتب مذكرة من سطرين للمدير عمّا تُظهره النتيجة.
14. تحدٍّ قصير — استعلام يبدو صحيحًا
نشاط خيالي: Cedar Lane. كتب زميل هذا الاستعلام ليحصل على "المنتجات الخمسة ذات أعلى هامش":
print(q("SELECT product_name, list_price - unit_cost AS margin FROM products LIMIT 5").to_string(index=False))
- قبل تشغيله، توقّع ما الخطأ فيه.
- شغّله واشرح لماذا لا تجيب النتيجة عن السؤال.
- أصلح الاستعلام وأثبت أن صفوفك الخمسة هي فعلًا أعلى خمسة هوامش.
- اكتب جملة واحدة يحتفظ بها زميلك كقاعدة.
سلّم عملك قبل أن تتلقى الملاحظات. لا يوجد حل منشور هنا.
15. المصادر
مطلوب (حوالي 30 دقيقة):
- Python docs:
sqlite3— DB-API 2.0 interface for SQLite databases، الدرس التمهيدي في أعلى الصفحة (الاتصال، والتنفيذ، والجلب) وsqlite3.sqlite_version. https://docs.python.org/3/library/sqlite3.html (فُتح في 19 أكتوبر 2026.) - SQLite: SELECT، النظرة العامة على
SELECTالبسيط وقسماORDER BYوLIMIT. https://www.sqlite.org/lang_select.html
موصى به:
- SQLite: Quirks, Caveats, and Gotchas In SQLite، القسمان الخاصان بالأنواع المرنة وبالتواريخ المخزَّنة كنص. https://www.sqlite.org/quirks.html
- pandas API:
read_sql_query، كيف تصبح نتيجة SQL جدول DataFrame، وكيف يمرّرparams=القيم. https://pandas.pydata.org/docs/reference/api/pandas.read_sql_query.html
مرجع (ارجع إليه عند الحاجة):
- SQLite home page، لأحدث إصدار وفهرس التوثيق. https://sqlite.org/
متابعة مفتوحة (D.World): لم يُختبر بعد تثبيت إصدار SQLite المرفق مع Python على Windows وmacOS وLinux لهذه الدورة (SN03). ولهذا يطلب L43 من كل متعلّم طباعته والإبلاغ عنه.
16. شخصية تتابعها / قدوة
D. Richard Hipp (اختارته D.World للدروس L43–L46).
- حقيقة مُتحقَّق منها: طوّر الدكتور D. Richard Hipp قاعدة SQLite في الأصل وأصدرها عام 2000 (بيان صحفي من SQLite Consortium، مذكور في خريطة D.World للمرحلة 3).
- لماذا توصي به D.World: قاعدة البيانات التي تستعلم منها، والموجودة داخل Python والهواتف والمتصفحات، تأتي من مشروعه. وتوثيق SQLite الذي تقرؤه في U11 واضح على نحو غير مألوف بشأن خصوصياتها.
- ملاحظة صادقة: لا نذكر إلا الحقيقة المذكورة أعلاه.
- جرّب هذا: افتح الصفحة الرئيسية لـ SQLite وابحث عن صفحة "Quirks". أي خصوصية من هذا الدرس تصفها أولًا؟
17. المصطلحات
| المصطلح | معناه في هذا الدرس |
|---|---|
| قاعدة بيانات علائقية (Relational database) | بيانات مخزّنة في جداول ترتبط بقيم مفاتيح مشتركة |
| جدول (Table) | مجموعة صفوف من نوع واحد من الأشياء، بأعمدة مسمّاة |
| المفتاح الأساسي (Primary key) | العمود الذي تميّز قيمته كل صف تمييزًا فريدًا، مثل orders.order_id |
| المفتاح الأجنبي (Foreign key) | عمود يحمل المفتاح الأساسي لجدول آخر، مثل orders.customer_id |
| المخطط (Schema) | جداول قاعدة البيانات وأعمدتها وأنواعها ومفاتيحها |
| الاستعلام (Query) | طلب بيانات مكتوب بلغة SQL |
| SQLite | محرّك قواعد بيانات يحفظ قاعدة البيانات كلها في ملف واحد، ومدمج في Python باسم sqlite3 |
| الاسم البديل (Alias) | اسم جديد لعمود في النتيجة، يُكتب بـ AS |
| DISTINCT | لا يُبقي في النتيجة إلا الصفوف (أو تركيبات القيم) المختلفة |
| مجموعة النتائج (Result set) | الصفوف والأعمدة التي يُرجعها الاستعلام |
للمراجعة: DataFrame والأعمدة (L25) · الترتيب (L26) · الدفاتر (L21).
18. تأمّل
- اشرح لزميل، دون SQL، لماذا يحتاج "أعلى عشرة" إلى ترتيب وتحديد عدد معًا.
- أي مستوى تفصيل ستستخدم لعدّ الطلبات، وأيه لعدّ المنتجات المبيعة؟ ولماذا؟
- ما الذي فاجأك في طريقة تخزين SQLite للتواريخ؟
19. الخطوة التالية
أصبحت الآن قادرًا على قراءة مخطط وكتابة استعلامات SELECT مرتّبة ومحدودة ومقروءة. والتالي:
- L44 — تصفية الصفوف: WHERE والمنطق وNULL (Filtering Rows: WHERE, Logic & NULL). نادرًا ما تريد الأسئلة كل الصفوف. ستُرجع بالضبط الصفوف التي يصفها الطلب، وتتعامل مع القيم المفقودة بشكل صحيح، وتمرّر القيم من Python بأمان.
20. أدلة التقييم
| الدليل | ما الذي يُظهره | كيف يُقيَّم |
|---|---|---|
| فحص البيئة + استعلامات على قاعدة بيانات لم يرها من قبل (التدريب المستقل) | يقرأ مخططًا غير مألوف؛ ويحدد مستوى التفصيل؛ ويجيب عن أسئلة "اعرض لي" بالأعمدة والترتيب والحد الصحيحة؛ ويتحقق من كل إجابة | دليل التقييم L43-IP |
| التدريب الموجَّه (المنتجات) | فحص المخطط، والأسماء البديلة، والعمود المحسوب، والترتيب والحد، وفحص العدد | دليل التقييم L43-GP |
| اختبار الفهم (Q1–Q8) | المفاتيح، ومستوى التفصيل، والأسماء البديلة، وORDER BY مع LIMIT، وDISTINCT، والإصدار | 7 إجابات صحيحة أو أكثر من 8 |
| مهمة العمل والتحدي القصير | ملف استعلامات فيه استعلام السعر مع الضريبة والمذكرة؛ ويكتشف ORDER BY مفقودًا | دليلا التقييم L43-WT وL43-MC |
المراجع
- Python Software Foundation. sqlite3 — DB-API 2.0 interface for SQLite databases. https://docs.python.org/3/library/sqlite3.html (PY-SQLITE). يدعم: الاتصال، و
sqlite3.sqlite_version. - SQLite. SELECT. https://www.sqlite.org/lang_select.html (SQ-SEL). يدعم: SELECT والأسماء البديلة وORDER BY وLIMIT وDISTINCT؛ وعدم ضمان أي ترتيب من دون ORDER BY.
- SQLite. Quirks, Caveats, and Gotchas In SQLite. https://www.sqlite.org/quirks.html (SQ-QRK). يدعم: الأنواع المرنة، والتواريخ كنص.
- pandas development team. pandas.read_sql_query. https://pandas.pydata.org/docs/reference/api/pandas.read_sql_query.html (P-RSQL). يدعم: نتائج SQL كجداول DataFrame، و
params=. - SQLite. Home page. https://sqlite.org/ (SQ-HOME). يدعم: معلومات الإصدارات.
- D.World Stage 3 Detailed Lesson Map v0.9 (صف L43؛ حقيقة القدوة؛ SN03؛ U11 تستخدم طريقة العرض المرفقة، وعمليات الربط تبدأ في L47). وثيقة داخلية.
L44 · Filtering Rows: WHERE, Logic & NULL Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
L45 · Aggregating with GROUP BY & HAVING Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
L46 · CASE, COALESCE, NULLIF & Simple Subqueries Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
U11 Exit Check — Operations Question Set Membersللمشتركين
The exit brief is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
تكليف نهاية الوحدة ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
U12 · SQL for Real AnalyticsSQL للتحليل الحقيقي
Stage 3المرحلة 3Free: the first lesson. The other lessons, the exit brief and the practice files are for Starter and Membership.
مجاني: الدرس الأول. باقي الدروس وتكليف نهاية الوحدة وملفات التمرين لأصحاب باقة البداية والاشتراك.
Download U12 practice files (.zip, 182 KB)حمّل ملفات تمرين U12 (zip، 182 KB)
Unzip into a short folder such as D:\DWorld on Windows. The zip has only learner files: no answers.
فك الضغط في مجلد قصير مثل D:\DWorld على ويندوز. الملف يحتوي ملفات المتعلم فقط بدون إجابات.
L47 · Joining Tables: INNER & LEFT JOIN with Row-Count ChecksL47 · ربط الجداول: INNER JOIN وLEFT JOIN مع فحوص عدد الصفوف Freeمجاني
L47 — Joining Tables: INNER & LEFT JOIN with Row-Count Checks
Learner file. Answers and model solutions are in a separate instructor-only file. Version: U12 Production v2 · Content date: 20 October 2026. Time estimates are planning estimates until learners have tested them. The code in this lesson was run with the course setup from L21 (Python 3.13, pandas 3.0 and NumPy 2.5; SciPy 1.18 and Matplotlib 3.11 where used) on Linux and on Windows 11 on 4 November 2026 (see the Code Test Report). The queries need SQLite 3.25 or later.
All businesses are fictional and all data is synthetic. Customers are pseudonymous IDs only.
1. Lesson Identity
| Field | Value |
|---|---|
| Title | Joining Tables: INNER & LEFT JOIN with Row-Count Checks |
| Stage | Stage 3 — Data Analysis |
| Unit | U12 — SQL for Real Analytics |
| Lesson ID | L47 |
| Skills | Join keys and cardinality; INNER and LEFT JOIN; table aliases; anti-joins; ON vs WHERE with LEFT JOIN; fan-out; join then GROUP BY; row-count proofs |
| Difficulty | Intermediate |
| Estimated time | About 2.5 hours (planning estimate, unvalidated) |
| Assessment level | Practice / Application |
| Prerequisites | U11 (L43–L46), L28 (merge with validate= and indicator=), L43 (primary and foreign keys). |
Time breakdown (planning estimate):
- Concept and worked example: 60 min.
- Guided practice: 25 min.
- Independent practice: 30 min.
- Knowledge check: 10 min.
- AI in Practice and Verification: 10 min (optional).
- Workplace task and mini challenge: 15 min.
What you need: your L21 environment and practice_files (L47_starter.ipynb; u12_data/cedar_lane_u12.db). Nothing new to install.
The U12 database. cedar_lane_u12.db holds a full year of Cedar Lane data (October 2025 to mid-October 2026). It has the U11 tables (regions, products, customers, orders, order_items) plus pickup_points, product_costs (cost history) and calendar. In U11 you used a ready-made view; from now on you build the joins yourself.
2. Learning Objective
By the end of this lesson, you will be able to:
- join two and three tables with
INNER JOINandLEFT JOINon stated keys; - predict a join's row count from the keys before running it;
- prove, with row counts and totals, that no rows were lost or duplicated unexpectedly;
- find unmatched rows with an anti-join.
D.World approved objective (verbatim): Join two and three tables with INNER and LEFT JOIN on stated keys and prove, with row counts, that no rows were lost or duplicated unexpectedly.
3. Why It Matters
Almost every real question needs more than one table. "Revenue by product category and region" needs the order lines (quantities and prices), the products (categories) and the orders (regions). A join combines them.
Joins are also the most common source of wrong numbers in analytics, because a join can change the number of rows without any error:
- an
INNER JOINsilently drops rows that have no match, so revenue disappears from the report; - joining on a key that is not unique on one side multiplies rows (fan-out), so revenue is counted twice;
- a
LEFT JOINfollowed by the wrongWHEREturns back into an inner join.
You met the same risks with merge in L28. In SQL the habits are the same: know the keys, predict the row count, check it.
Key principle: before every join, state the key and the relationship; after every join, prove the row count and the total.
4. Concept
import sqlite3
import pandas as pd
con = sqlite3.connect("u12_data/cedar_lane_u12.db")
def q(sql, params=()):
return pd.read_sql_query(sql, con, params=params)
print(q("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name").T.to_string(header=False))
name calendar customers order_items orders pickup_points product_costs products regions
Part A — Keys and relationships (cardinality)
A join matches rows of two tables where a key has the same value. Before joining, ask how many rows on each side can share a key value. That is the cardinality of the relationship:
- one-to-one: each key appears at most once on both sides;
- one-to-many: unique on one side, repeated on the other (
customers.customer_id→ manyorders); - many-to-many: repeated on both sides; joining directly multiplies rows.
Check uniqueness with a count, before you join:
print(q("""
SELECT 'orders.order_id' AS key_column, COUNT(*) AS rows, COUNT(DISTINCT order_id) AS distinct_values FROM orders
UNION ALL
SELECT 'order_items.order_id', COUNT(*), COUNT(DISTINCT order_id) FROM order_items
UNION ALL
SELECT 'product_costs.product_id', COUNT(*), COUNT(DISTINCT product_id) FROM product_costs
""").to_string(index=False))
key_column rows distinct_values
orders.order_id 1000 1000
order_items.order_id 2165 990
product_costs.product_id 28 14
UNION ALL stacks the results of several queries with the same columns. orders.order_id is unique (rows = distinct values). order_items.order_id repeats (one order, several lines). product_costs.product_id also repeats: each product has two cost rows (a cost change during the year). Keep that in mind for Part E.
Part B — INNER JOIN and table aliases
INNER JOIN keeps only pairs of rows that match on the key:
lines = q("""
SELECT oi.line_id, oi.order_id, o.region_code, oi.product_id, oi.quantity, oi.unit_price_paid
FROM order_items AS oi
INNER JOIN orders AS o ON o.order_id = oi.order_id
ORDER BY oi.line_id
LIMIT 3
""")
print(lines.to_string(index=False))
line_id order_id region_code product_id quantity unit_price_paid
1 O-20001 WEB P09 3 2.80
2 O-20002 NOR P07 1 3.60
3 O-20003 WEB P06 1 5.13
FROM order_items AS oigives the table a short table alias;oi.quantitythen means "thequantitycolumn oforder_items". Aliases are required when both tables have a column with the same name (order_id).ON o.order_id = oi.order_idstates the key.JOINon its own meansINNER JOIN; writingINNERmakes your intention visible.
Predict, then check. Each line has exactly one order (orders.order_id is unique), so joining lines to orders should keep one row per line:
n_lines = q("SELECT COUNT(*) AS n FROM order_items")["n"].iloc[0]
n_join = q("SELECT COUNT(*) AS n FROM order_items AS oi INNER JOIN orders AS o ON o.order_id = oi.order_id")["n"].iloc[0]
print("lines:", n_lines, "| after join:", n_join)
lines: 2165 | after join: 2165
Part C — LEFT JOIN keeps every row of the left table
LEFT JOIN keeps every row of the left table; where there is no match, the right table's columns are NULL.
inner = q("SELECT COUNT(*) AS n FROM orders AS o INNER JOIN customers AS c ON c.customer_id = o.customer_id")["n"].iloc[0]
left = q("SELECT COUNT(*) AS n FROM orders AS o LEFT JOIN customers AS c ON c.customer_id = o.customer_id")["n"].iloc[0]
print("orders: 1000 | inner:", inner, "| left:", left)
orders: 1000 | inner: 999 | left: 1000
One order disappears with the inner join. Which one? An anti-join finds the rows of the left table that have no match: LEFT JOIN and keep the rows where the right key is NULL.
print(q("""
SELECT o.order_id, o.customer_id, o.order_date
FROM orders AS o
LEFT JOIN customers AS c ON c.customer_id = o.customer_id
WHERE c.customer_id IS NULL
""").to_string(index=False))
order_id customer_id order_date
O-20334 C999 2026-02-23
The order points to a customer that does not exist in customers. How can the database allow that? SQLite does not enforce foreign keys unless you switch them on (PRAGMA foreign_keys = ON for each connection). The schema says REFERENCES customers, but nothing stopped the bad value. Whether this is an error to fix is a data-quality question (U09); your job here is to notice it and decide which join the question needs:
- "revenue of all orders" → keep the order (
LEFT JOIN, or do not join customers at all); - "revenue by customer segment" → the orphan order has no segment; report it as a separate "unknown" line rather than letting it vanish.
Part D — ON versus WHERE with LEFT JOIN
With a LEFT JOIN, a condition on the right table means different things in ON and in WHERE:
on_version = q("""
SELECT p.pickup_id, COUNT(o.order_id) AS orders_march_2026
FROM pickup_points AS p
LEFT JOIN orders AS o
ON o.pickup_id = p.pickup_id
AND o.order_date >= '2026-03-01' AND o.order_date < '2026-04-01'
GROUP BY p.pickup_id ORDER BY p.pickup_id
""")
where_version = q("""
SELECT p.pickup_id, COUNT(o.order_id) AS orders_march_2026
FROM pickup_points AS p
LEFT JOIN orders AS o ON o.pickup_id = p.pickup_id
WHERE o.order_date >= '2026-03-01' AND o.order_date < '2026-04-01'
GROUP BY p.pickup_id ORDER BY p.pickup_id
""")
print(on_version.to_string(index=False))
print(where_version.to_string(index=False))
pickup_id orders_march_2026
PK-CEN 8
PK-HAR 7
PK-L7 0
PK-NOR 16
PK-STA 7
pickup_id orders_march_2026
PK-CEN 8
PK-HAR 7
PK-NOR 16
PK-STA 7
- In
ON, the date condition decides which orders match; every pickup point is kept, and one shows 0 orders in March. - In
WHERE, the condition runs after the join and removes the rows whereo.order_dateis NULL, which are exactly the unmatched pickup points. TheLEFT JOINhas quietly become an inner join, and the point with no March orders disappears.
Rule: with LEFT JOIN, conditions on the right table go in ON if you want to keep unmatched left rows. Use COUNT(o.order_id), not COUNT(*): COUNT(*) would count the NULL row of an unmatched point as 1.
Part E — Fan-out: a key that is not unique
Joining products to product_costs on product_id alone matches each product twice (two cost rows):
fan = q("""
SELECT COUNT(*) AS n
FROM order_items AS oi
JOIN product_costs AS pc ON pc.product_id = oi.product_id
""")["n"].iloc[0]
print("lines:", n_lines, "| after joining costs on product_id only:", fan)
lines: 2165 | after joining costs on product_id only: 4330
Every line is duplicated, so any revenue summed after this join is doubled. This is fan-out. The fix is a complete key: the cost that was valid on the order date. That needs the cost row with the latest valid_from on or before the order date, which you will write cleanly with a CTE in L48. For now the lesson is: check uniqueness of the right-hand key before joining, and compare the row count after.
Part F — Three tables, then GROUP BY
Join first, check, then aggregate:
three = q("""
SELECT p.category, o.region_code, ROUND(SUM(oi.quantity * oi.unit_price_paid), 2) AS revenue
FROM order_items AS oi
INNER JOIN orders AS o ON o.order_id = oi.order_id
INNER JOIN products AS p ON p.product_id = oi.product_id
WHERE o.region_code = 'HAR'
GROUP BY p.category, o.region_code
ORDER BY revenue DESC
""")
print(three.to_string(index=False))
category region_code revenue
Drinks HAR 2198.21
Pantry HAR 1822.04
Fresh HAR 434.22
Household HAR 173.85
The database first builds the joined rows (one per line, with the order's region and the product's category), then filters, then groups. Reconcile: the total of the grouped revenue must equal the line-level total for the same filter, computed without the products join. If it is larger, a join fanned out; if smaller, a join dropped rows.
Optional (SQLite 3.39+): RIGHT JOIN and FULL OUTER JOIN exist in newer SQLite versions. Any RIGHT JOIN can be written as a LEFT JOIN with the tables swapped, which works on every version, so this course uses LEFT JOIN only.
Part G — Reproduce a join with pandas merge(validate=...)
The same checks you used in L28 confirm a SQL join independently:
oi = q("SELECT * FROM order_items")
o = q("SELECT order_id, region_code FROM orders")
m = oi.merge(o, on="order_id", how="inner", validate="many_to_one")
print(len(m), "rows; pandas confirms order_id is unique in orders (validate passed)")
try:
oi.merge(q("SELECT * FROM product_costs"), on="product_id", validate="many_to_one")
except Exception as e:
print(type(e).__name__, "-", str(e)[:60])
2165 rows; pandas confirms order_id is unique in orders (validate passed)
MergeError - Merge keys are not unique in right dataset; not a many-to-on
validate="many_to_one" raises an error when the right key is not unique, which is exactly the fan-out you saw in Part E.
5. Worked Example
Cedar Lane (fictional), synthetic data. The operations lead asks: "How many orders did each pickup point handle in the third quarter of 2026 (July–September), and how much revenue did they bring? Include every pickup point, even if it had none."
Step 1 — Keys and relationships. pickup_points.pickup_id is unique (5 rows) → one-to-many to orders.pickup_id. orders.order_id is unique → one-to-many to order_items. Online orders have no pickup point (pickup_id NULL); they are outside this question.
Step 2 — Predict. One row per pickup point (5 rows), because we group by it, and every point is kept by LEFT JOIN.
Step 3 — Query (two LEFT JOINs, date condition in ON).
pickup = q("""
SELECT p.pickup_id, p.pickup_name,
COUNT(DISTINCT o.order_id) AS orders,
ROUND(COALESCE(SUM(oi.quantity * oi.unit_price_paid), 0), 2) AS revenue
FROM pickup_points AS p
LEFT JOIN orders AS o
ON o.pickup_id = p.pickup_id
AND o.order_date >= '2026-07-01' AND o.order_date < '2026-10-01'
LEFT JOIN order_items AS oi ON oi.order_id = o.order_id
GROUP BY p.pickup_id, p.pickup_name
ORDER BY revenue DESC
""")
print(pickup.to_string(index=False))
pickup_id pickup_name orders revenue
PK-HAR Harbor shop counter 65 1385.42
PK-NOR North shop counter 53 944.29
PK-CEN Central shop counter 53 929.80
PK-STA Station shop counter 19 291.49
PK-L7 Station parcel locker 7 12 207.80
COUNT(DISTINCT o.order_id) counts orders even though the second join creates one row per line. COALESCE(SUM(...), 0) shows 0 revenue for a point with no orders; here the default is correct, because "no orders" really means "no revenue" (compare L46, where a default would have hidden an unknown).
Step 4 — Reconcile with tables that need no join to pickup points.
check = q("""
SELECT COUNT(DISTINCT o.order_id) AS orders, ROUND(SUM(oi.quantity * oi.unit_price_paid), 2) AS revenue
FROM orders AS o
LEFT JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE o.pickup_id IS NOT NULL AND o.order_date >= '2026-07-01' AND o.order_date < '2026-10-01'
""")
print(check.to_string(index=False))
print("match:", pickup["orders"].sum() == check["orders"].iloc[0], round(pickup["revenue"].sum(), 2) == check["revenue"].iloc[0])
orders revenue
202 3758.8
match: True True
Step 5 — Join log.
Keys:
pickup_points.pickup_idunique (5) →orders.pickup_id;orders.order_idunique →order_items.order_id(many). Result: 5 rows as predicted. Totals equal the pickup orders in Q3 computed without the pickup table. Note: orders with no lines (cancelled before picking) count as orders with 0 revenue.
6. Visual Explanation
orders (1000) customers (260) INNER JOIN → 999 rows (orphan O-… dropped)
┌────────┬──────────┐ ┌──────────┬─────────┐ LEFT JOIN → 1000 rows (orphan kept, c.* = NULL)
│O-… │C017 │──────▶ │C017 │Regular │ ANTI-JOIN → LEFT JOIN … WHERE c.customer_id IS NULL
│O-… │C999 ? │──╳ │… │… │
└────────┴──────────┘ └──────────┴─────────┘
FAN-OUT: order_items (2165) ⋈ product_costs ON product_id only → each product has 2 cost rows → 4330 rows → revenue ×2
LEFT JOIN + condition on the right table: … ON key AND o.date in March → unmatched kept (0)
… ON key WHERE o.date in March → unmatched removed (inner join!)
CHECK EVERY JOIN: key unique where it should be? │ predicted rows = actual rows │ Σ after join = Σ before join
Alt text: At the top, the orders table with 1000 rows is joined to customers; one order points to a customer that does not exist. An inner join gives 999 rows and drops it; a left join keeps all 1000 with NULL customer columns; an anti-join, a left join keeping rows where the customer key is NULL, finds it. In the middle, fan-out: order items joined to product costs on product ID alone match two cost rows per product and double the rows and the revenue. Below, a left join with a condition on the right table: in ON, unmatched rows are kept with zero; in WHERE, they are removed and the join behaves like an inner join. At the bottom, the three checks for every join: is the key unique where it should be, does the actual row count equal the prediction, and does the total after the join equal the total before.
7. Guided Practice
Open L47_starter.ipynb.
- Check key uniqueness:
customers.customer_id,orders.order_id,products.product_id(rows vs distinct values). - Predict, then join
orderstocustomers(INNER). Record the row count. Then do it as a LEFT JOIN and explain the difference. - Predict, then join
order_itemstoordersand toproducts(two INNER JOINs). Record the row count after each join. - Compare
SUM(quantity * unit_price_paid)after the three-table join with the same sum onorder_itemsalone. - Write a join log: for each join, the key, the relationship, the predicted and actual row counts.
- Run Restart Kernel and Run All Cells….
8. Independent Practice
Category revenue and the unsold products. The commercial lead asks for two things:
"Revenue by product category and region for the whole year, and a list of products that have never sold."
- Before writing any join, check the keys you will use and predict the row count of each join.
- Write the revenue query (one row per category and region).
- Write the never-sold query (an anti-join) with product ID, name, category and launch date.
- Prove your results: the revenue table's total equals the line-level total; the never-sold list is confirmed a second way (for example with
NOT EXISTS-free logic in pandas: product IDs minus sold product IDs). - Document the key checks you ran and their results.
Work in L47_starter.ipynb (Part 2).
9. Knowledge Check — Joins
Q1. orders.order_id is unique and order_items.order_id repeats. What is the relationship from orders to order_items?
- A) none
- B) one-to-one
- C) one-to-many
- D) many-to-many
Q2. You join 2,000 order lines to orders on order_id (unique in orders). With an INNER JOIN, how many rows do you expect if every line has an order?
- A) It depends on the number of customers
- B) 1,000
- C) 4,000
- D) 2,000
Q3. Which query finds orders whose customer does not exist in customers?
- A)
SELECT … FROM orders WHERE customer_id = NULL - B)
SELECT … FROM orders o INNER JOIN customers c ON c.customer_id = o.customer_id WHERE c.customer_id IS NULL - C)
SELECT … FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id - D)
SELECT … FROM orders o LEFT JOIN customers c ON c.customer_id = o.customer_id WHERE c.customer_id IS NULL
Q4. A LEFT JOIN from pickup_points to orders has WHERE o.order_date >= '2026-03-01'. What happens to pickup points with no March orders?
- A) They cause an error
- B) They are duplicated
- C) They are kept with 0
- D) They are removed, because
o.order_dateis NULL for them and the WHERE condition is not true
Q5. After joining order_items (2,165 rows) to a cost table on product_id, you get 4,330 rows. What happened?
- A) INNER JOIN always doubles rows
- B) Fan-out: each product has two rows in the cost table
- C) The join is correct
- D) The cost table is empty
Q6. Why does SELECT COUNT(*) … FROM pickup_points p LEFT JOIN orders o … GROUP BY p.pickup_id show 1 for a point with no orders?
- A) The point really has one order
- B) COUNT(*) counts the one row the LEFT JOIN keeps for the unmatched point; use
COUNT(o.order_id) - C) GROUP BY adds a row
- D) SQLite has a bug
Q7. In SQLite, does REFERENCES customers(customer_id) in a table definition stop an order from pointing to a missing customer?
- A) Only if foreign keys are switched on for the connection (
PRAGMA foreign_keys = ON) - B) Only for TEXT keys
- C) Always
- D) Never, foreign keys do not exist in SQLite
Q8. After a three-table join and GROUP BY, the grouped revenue total is higher than the line-level total. What is the most likely cause?
- A) Rounding
- B) NULL values
- C) A join fanned out (a non-unique key on one side)
- D) An INNER JOIN dropped rows
10. AI in Practice (optional, bounded)
Goal: ask an AI assistant for a three-table join and check it before trusting it.
- Give it the column lists of
order_items,ordersandproductsand ask:
"Write a SQLite query for revenue (quantity × unit_price_paid) by product category and month (from order_date)."
- Before running it, write the keys it should use and the row count of the joined rows (before grouping).
- Run it and follow Section 11.
Boundaries:
- AI is optional.
- Share column names and table names, not data.
- The AI does not know which keys are unique in your data. Fan-out and dropped rows are yours to check.
11. AI Verification
- Keys: are the join conditions on the right columns (
order_id,product_id)? Did it use any other table? - Row count: run its
FROM … JOIN …part withCOUNT(*). Does it match your prediction? - Total: does its grouped revenue add up to the line-level total?
- pandas: reproduce one join with
merge(validate=...). - Write down one thing it got right and one thing you checked that it did not mention.
12. Business Lens
Revenue reports are joins. A category-by-region revenue table, a customer-segment summary, a pickup-point league table: each combines several tables. When a join fans out, a region's revenue looks 20% higher and a bonus is paid on it; when an inner join drops orphan rows, a supplier invoice is never matched and nobody notices.
The professional habit is a join log next to every report: the keys, the relationship, the predicted and actual row counts, and the reconciliation of totals. It takes minutes and it is the first thing a reviewer looks for.
13. Workplace Task — The Segment Revenue Table
Fictional business: Cedar Lane. The marketing lead wants revenue and number of orders by customer segment for the whole year.
- Write the query (lines → orders → customers). Decide what happens to the orphan order and to customers with no recorded segment; show both as visible lines (for example "unknown customer", "segment not recorded") rather than letting them disappear.
- Reconcile the table with the total revenue of all lines.
- Write a three-line note for the marketing lead, including how the two special lines should be read.
14. Mini Challenge — The Report That Grew
Fictional business: Cedar Lane. A colleague added unit costs to a revenue report:
print(q("""
SELECT ROUND(SUM(oi.quantity * oi.unit_price_paid), 2) AS revenue,
ROUND(SUM(oi.quantity * pc.unit_cost), 2) AS cost
FROM order_items AS oi
JOIN product_costs AS pc ON pc.product_id = oi.product_id
""").to_string(index=False))
- Before running it, predict what is wrong with both numbers.
- Run it, then compute the true revenue from
order_itemsalone. Explain the difference with row counts. - Describe in words what the correct join to
product_costsmust match on (you will write it in L48). - Write one sentence your colleague can keep as a rule.
Submit before feedback. No solution is provided here.
15. Resources
Required (about 30 min):
- SQLite: SELECT, the section on the FROM clause and join operators. https://www.sqlite.org/lang_select.html (Opened 20 Oct 2026.)
Recommended:
- SQLite: Quirks, Caveats, and Gotchas In SQLite, the section on foreign keys being off by default. https://www.sqlite.org/quirks.html
- SQLite Release 3.39.0 (2022-06-25), the release that added RIGHT and FULL OUTER JOIN (optional in this course). https://sqlite.org/releaselog/3_39_0.html
Open follow-up (D.World, SN03): the SQLite version bundled with Python has not yet been install-tested on Windows, macOS and Linux for this course. This lesson uses only INNER and LEFT JOIN, which work on every version.
16. Person to Follow / Role Model
D. Richard Hipp (mapped by D.World for L47–L50).
- Verified fact: SQLite was originally developed and released in 2000 by Dr. D. Richard Hipp (SQLite Consortium press release, cited in the D.World Stage 3 map).
- Why D.World recommends him: the SQLite release log records exactly when each feature (such as RIGHT JOIN in 3.39.0) arrived, which is how you decide what is safe to use.
- Honest note: we cite only the fact above.
- Try this: open the 3.39.0 release log and find the line about RIGHT and FULL OUTER JOIN.
17. Glossary
| Term | Meaning in this lesson |
|---|---|
| INNER JOIN | Keeps only pairs of rows that match on the key |
| LEFT JOIN | Keeps every row of the left table; unmatched right columns are NULL |
| Anti-join | Finds left rows with no match: LEFT JOIN … WHERE right.key IS NULL |
| Fan-out | Rows multiplied by a join because the key is not unique on one side |
| Cardinality | How many rows on each side can share a key value: one-to-one, one-to-many, many-to-many |
| Table alias | A short name for a table in a query, e.g. FROM orders AS o |
Review: join and merge, validate= and indicator= (L28) · primary and foreign keys (L43).
18. Reflection
- Explain to a colleague, without SQL, why a join can make revenue go up.
- When is an INNER JOIN the right choice, and when a LEFT JOIN?
- What will your join log contain from now on?
19. Next Step
You can now join tables and prove the result. Next:
- L48 — Subqueries & CTEs. Multi-step questions (like the cost valid on each order date) become readable, testable steps.
20. Assessment Evidence
| Evidence | What it shows | How it is judged |
|---|---|---|
| Category × region revenue + never-sold list + key checks (Independent Practice) | Predicts row counts; correct joins; anti-join; totals reconciled; key checks documented | Rubric L47-IP |
| Guided practice (join log) | Key uniqueness; predicted vs actual counts; totals before and after | Rubric L47-GP |
| Knowledge check (Q1–Q8) | Cardinality, INNER vs LEFT, anti-join, ON vs WHERE, fan-out, foreign keys | 7 or more of 8 correct |
| Workplace task and mini challenge | Special lines made visible; fan-out explained | Rubrics L47-WT, L47-MC |
References
- SQLite. SELECT (FROM clause, joins). https://www.sqlite.org/lang_select.html (SQ-SEL). Supports: INNER, LEFT JOIN, UNION ALL.
- SQLite. Release 3.39.0. https://sqlite.org/releaselog/3_39_0.html (SQ-REL339). Supports: RIGHT and FULL OUTER JOIN availability.
- SQLite. Quirks, Caveats, and Gotchas In SQLite. https://www.sqlite.org/quirks.html (SQ-QRK). Supports: foreign keys off by default.
- pandas development team. pandas.DataFrame.merge (validate=). Used as reviewed in L28.
- D.World Stage 3 Detailed Lesson Map v0.9 (L47 row; SN03; role model fact). Internal.
L47 — ربط الجداول: INNER JOIN وLEFT JOIN مع فحوص عدد الصفوف
ملف المتعلّم. الإجابات والحلول النموذجية في ملف منفصل خاص بالمدرّس فقط. الإصدار: U12 Production v2 · تاريخ المحتوى: 20 أكتوبر 2026. تقديرات الوقت تقديرات للتخطيط إلى أن يختبرها المتعلّمون. الكود في هذا الدرس شُغّل باستخدام تجهيزات الدورة من L21 (Python 3.13 وpandas 3.0 وNumPy 2.5؛ وSciPy 1.18 وMatplotlib 3.11 حيث استُخدمتا) على Linux وعلى Windows 11 في 4 نوفمبر 2026 (راجع تقرير اختبار الكود Code Test Report). وتحتاج الاستعلامات إلى SQLite 3.25 أو أحدث.
كل الأنشطة التجارية خيالية، وكل البيانات مصطنعة. والعملاء ممثَّلون بأرقام تعريف مستعارة فقط.
1. بطاقة الدرس
| البند | القيمة |
|---|---|
| العنوان | ربط الجداول: INNER JOIN وLEFT JOIN مع فحوص عدد الصفوف |
| المرحلة | المرحلة 3 — تحليل البيانات |
| الوحدة | U12 — SQL للتحليلات الحقيقية |
| رقم الدرس | L47 |
| المهارات | مفاتيح الربط وعلاقات التعدد (cardinality)؛ INNER JOIN وLEFT JOIN؛ الأسماء البديلة للجداول؛ الربط العكسي (anti-join)؛ ON مقابل WHERE مع LEFT JOIN؛ تضاعف الصفوف (fan-out)؛ الربط ثم GROUP BY؛ إثباتات عدد الصفوف |
| المستوى | متوسط |
| الوقت المتوقع | حوالي ساعتين ونصف (تقدير للتخطيط، غير مُتحقَّق منه) |
| مستوى التقييم | تدريب / تطبيق |
| المتطلبات السابقة | U11 (L43–L46)، وL28 (الدمج مع validate= وindicator=)، وL43 (المفاتيح الأساسية والأجنبية). |
تفصيل الوقت (تقدير للتخطيط):
- المفهوم والمثال المحلول: 60 دقيقة.
- التدريب الموجَّه: 25 دقيقة.
- التدريب المستقل: 30 دقيقة.
- اختبار الفهم: 10 دقائق.
- الذكاء الاصطناعي في التطبيق والتحقق منه: 10 دقائق (اختياري).
- مهمة العمل والتحدي القصير: 15 دقيقة.
ما تحتاج إليه: بيئتك من L21 ومجلد practice_files (L47_starter.ipynb؛ وu12_data/cedar_lane_u12.db). لا شيء جديد للتثبيت.
قاعدة بيانات U12. تحتوي cedar_lane_u12.db على سنة كاملة من بيانات Cedar Lane (من أكتوبر 2025 إلى منتصف أكتوبر 2026). وفيها جداول U11 (regions وproducts وcustomers وorders وorder_items) إضافةً إلى pickup_points وproduct_costs (تاريخ التكاليف) وcalendar. في U11 استخدمت طريقة عرض جاهزة؛ ومن الآن فصاعدًا تبني عمليات الربط بنفسك.
2. هدف التعلّم
في نهاية هذا الدرس ستكون قادرًا على أن:
- تربط جدولين وثلاثة جداول بـ
INNER JOINوLEFT JOINعلى مفاتيح مُعلنة؛ - تتوقع عدد صفوف عملية الربط من المفاتيح قبل تشغيلها؛
- تثبت، بأعداد الصفوف والإجماليات، أنه لم تضع صفوف ولم تتكرر على نحو غير متوقع؛
- تجد الصفوف غير المطابقة بربط عكسي (anti-join).
الهدف المعتمد من D.World (بنصّه): Join two and three tables with INNER and LEFT JOIN on stated keys and prove, with row counts, that no rows were lost or duplicated unexpectedly.
3. لماذا يهمّك هذا؟
يكاد كل سؤال حقيقي يحتاج إلى أكثر من جدول. فسؤال "الإيرادات حسب فئة المنتج والمنطقة" يحتاج إلى أسطر الطلبات (الكميات والأسعار)، والمنتجات (الفئات)، والطلبات (المناطق). والربط (join) يجمع بينها.
والربط أيضًا هو أشيع مصدر للأرقام الخاطئة في التحليلات، لأنه قد يغيّر عدد الصفوف دون أي خطأ:
INNER JOINيُسقط بصمت الصفوف التي لا مطابق لها، فتختفي إيرادات من التقرير؛- الربط على مفتاح غير فريد في أحد الجانبين يضاعف الصفوف (تضاعف الصفوف fan-out)، فتُحتسب الإيرادات مرتين؛
LEFT JOINمتبوعًا بـWHEREخاطئ يعود ربطًا داخليًا (inner join).
قابلت المخاطر نفسها مع merge في L28. وفي SQL العادات هي نفسها: اعرف المفاتيح، وتوقّع عدد الصفوف، وتحقّق منه.
المبدأ الأساسي: قبل كل ربط، صرّح بالمفتاح وبالعلاقة؛ وبعد كل ربط، أثبت عدد الصفوف والإجمالي.
4. المفهوم
import sqlite3
import pandas as pd
con = sqlite3.connect("u12_data/cedar_lane_u12.db")
def q(sql, params=()):
return pd.read_sql_query(sql, con, params=params)
print(q("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name").T.to_string(header=False))
name calendar customers order_items orders pickup_points product_costs products regions
الجزء A — المفاتيح والعلاقات (علاقات التعدد)
الربط يطابق صفوف جدولين حيث يكون لـمفتاح القيمة نفسها. وقبل الربط، اسأل: كم صفًا في كل جانب قد يشترك في قيمة مفتاح واحدة؟ هذه هي علاقة التعدد (cardinality):
- واحد لواحد (one-to-one): كل مفتاح يظهر مرة واحدة على الأكثر في الجانبين؛
- واحد لمتعدد (one-to-many): فريد في جانب، ومكرّر في الآخر (
customers.customer_id← طلباتordersكثيرة)؛ - متعدد لمتعدد (many-to-many): مكرّر في الجانبين؛ والربط المباشر يضاعف الصفوف.
تحقّق من التفرّد بعدّ، قبل أن تربط:
print(q("""
SELECT 'orders.order_id' AS key_column, COUNT(*) AS rows, COUNT(DISTINCT order_id) AS distinct_values FROM orders
UNION ALL
SELECT 'order_items.order_id', COUNT(*), COUNT(DISTINCT order_id) FROM order_items
UNION ALL
SELECT 'product_costs.product_id', COUNT(*), COUNT(DISTINCT product_id) FROM product_costs
""").to_string(index=False))
key_column rows distinct_values
orders.order_id 1000 1000
order_items.order_id 2165 990
product_costs.product_id 28 14
الأمر UNION ALL يرصّ نتائج عدة استعلامات لها الأعمدة نفسها. العمود orders.order_id فريد (الصفوف = القيم المختلفة). والعمود order_items.order_id مكرّر (طلب واحد، عدة أسطر). والعمود product_costs.product_id مكرّر أيضًا: لكل منتج صفّا تكلفة (تغيّرت التكلفة خلال السنة). تذكّر ذلك من أجل الجزء E.
الجزء B — INNER JOIN والأسماء البديلة للجداول
الأمر INNER JOIN لا يُبقي إلا أزواج الصفوف المتطابقة على المفتاح:
lines = q("""
SELECT oi.line_id, oi.order_id, o.region_code, oi.product_id, oi.quantity, oi.unit_price_paid
FROM order_items AS oi
INNER JOIN orders AS o ON o.order_id = oi.order_id
ORDER BY oi.line_id
LIMIT 3
""")
print(lines.to_string(index=False))
line_id order_id region_code product_id quantity unit_price_paid
1 O-20001 WEB P09 3 2.80
2 O-20002 NOR P07 1 3.60
3 O-20003 WEB P06 1 5.13
FROM order_items AS oiيعطي الجدول اسمًا بديلًا قصيرًا (table alias)؛ فيعنيoi.quantityبعد ذلك "العمودquantityفيorder_items". والأسماء البديلة إلزامية عندما يكون في الجدولين عمود بالاسم نفسه (order_id).ON o.order_id = oi.order_idيصرّح بالمفتاح.JOINوحدها تعنيINNER JOIN؛ وكتابةINNERتجعل نيّتك ظاهرة.
توقّع، ثم تحقّق. لكل سطر طلب واحد بالضبط (orders.order_id فريد)، فيجب أن يُبقي ربط الأسطر بالطلبات صفًا لكل سطر:
n_lines = q("SELECT COUNT(*) AS n FROM order_items")["n"].iloc[0]
n_join = q("SELECT COUNT(*) AS n FROM order_items AS oi INNER JOIN orders AS o ON o.order_id = oi.order_id")["n"].iloc[0]
print("lines:", n_lines, "| after join:", n_join)
lines: 2165 | after join: 2165
الجزء C — LEFT JOIN يُبقي كل صفوف الجدول الأيسر
الأمر LEFT JOIN يُبقي كل صف في الجدول الأيسر؛ وحيث لا يوجد مطابق، تكون أعمدة الجدول الأيمن NULL.
inner = q("SELECT COUNT(*) AS n FROM orders AS o INNER JOIN customers AS c ON c.customer_id = o.customer_id")["n"].iloc[0]
left = q("SELECT COUNT(*) AS n FROM orders AS o LEFT JOIN customers AS c ON c.customer_id = o.customer_id")["n"].iloc[0]
print("orders: 1000 | inner:", inner, "| left:", left)
orders: 1000 | inner: 999 | left: 1000
طلب واحد يختفي مع الربط الداخلي. أيّها؟ الربط العكسي (anti-join) يجد صفوف الجدول الأيسر التي ليس لها مطابق: LEFT JOIN ثم أبقِ الصفوف التي يكون فيها مفتاح الجدول الأيمن NULL.
print(q("""
SELECT o.order_id, o.customer_id, o.order_date
FROM orders AS o
LEFT JOIN customers AS c ON c.customer_id = o.customer_id
WHERE c.customer_id IS NULL
""").to_string(index=False))
order_id customer_id order_date
O-20334 C999 2026-02-23
هذا الطلب يشير إلى عميل غير موجود في customers. فكيف سمحت قاعدة البيانات بذلك؟ SQLite لا تفرض المفاتيح الأجنبية ما لم تفعّلها (PRAGMA foreign_keys = ON لكل اتصال). فالمخطط يقول REFERENCES customers، لكن لا شيء منع القيمة الخاطئة. وهل هذا خطأ يجب إصلاحه؟ هذا سؤال جودة بيانات (U09)؛ أما مهمتك هنا فأن تلاحظه وتقرر أي ربط يحتاجه السؤال:
- "إيرادات كل الطلبات" ← أبقِ الطلب (
LEFT JOIN، أو لا تربط بالعملاء أصلًا)؛ - "الإيرادات حسب شريحة العملاء" ← الطلب اليتيم ليس له شريحة؛ فأبلغ عنه في سطر مستقل "unknown" (غير معروف) بدلًا من أن تتركه يختفي.
الجزء D — ON مقابل WHERE مع LEFT JOIN
مع LEFT JOIN، يختلف معنى الشرط على الجدول الأيمن بين ON وWHERE:
on_version = q("""
SELECT p.pickup_id, COUNT(o.order_id) AS orders_march_2026
FROM pickup_points AS p
LEFT JOIN orders AS o
ON o.pickup_id = p.pickup_id
AND o.order_date >= '2026-03-01' AND o.order_date < '2026-04-01'
GROUP BY p.pickup_id ORDER BY p.pickup_id
""")
where_version = q("""
SELECT p.pickup_id, COUNT(o.order_id) AS orders_march_2026
FROM pickup_points AS p
LEFT JOIN orders AS o ON o.pickup_id = p.pickup_id
WHERE o.order_date >= '2026-03-01' AND o.order_date < '2026-04-01'
GROUP BY p.pickup_id ORDER BY p.pickup_id
""")
print(on_version.to_string(index=False))
print(where_version.to_string(index=False))
pickup_id orders_march_2026
PK-CEN 8
PK-HAR 7
PK-L7 0
PK-NOR 16
PK-STA 7
pickup_id orders_march_2026
PK-CEN 8
PK-HAR 7
PK-NOR 16
PK-STA 7
- في
ON، يحدد شرط التاريخ أي الطلبات تُطابَق؛ فتبقى كل نقاط الاستلام، وتُظهر إحداها 0 طلبات في مارس. - في
WHERE، يعمل الشرط بعد الربط ويحذف الصفوف التي يكون فيهاo.order_dateهو NULL، وهي بالضبط نقاط الاستلام غير المطابقة. فقد أصبحLEFT JOINربطًا داخليًا بهدوء، واختفت النقطة التي ليس لها طلبات في مارس.
القاعدة: مع LEFT JOIN، توضع الشروط على الجدول الأيمن في ON إذا أردت إبقاء صفوف الجدول الأيسر غير المطابقة. واستخدم COUNT(o.order_id) لا COUNT(*): لأن COUNT(*) سيعدّ صف NULL الخاص بالنقطة غير المطابقة على أنه 1.
الجزء E — تضاعف الصفوف: مفتاح غير فريد
ربط products بـ product_costs على product_id وحده يطابق كل منتج مرتين (صفّا تكلفة):
fan = q("""
SELECT COUNT(*) AS n
FROM order_items AS oi
JOIN product_costs AS pc ON pc.product_id = oi.product_id
""")["n"].iloc[0]
print("lines:", n_lines, "| after joining costs on product_id only:", fan)
lines: 2165 | after joining costs on product_id only: 4330
كل سطر تكرّر، فأي إيرادات تُجمع بعد هذا الربط تكون مضاعفة. هذا هو تضاعف الصفوف (fan-out). والإصلاح مفتاح كامل: التكلفة التي كانت سارية في تاريخ الطلب. وهذا يحتاج إلى صف التكلفة صاحب أحدث valid_from في تاريخ الطلب أو قبله، وستكتبه بشكل نظيف باستخدام CTE في L48. أما الدرس الآن فهو: تحقّق من تفرّد مفتاح الجانب الأيمن قبل الربط، وقارن عدد الصفوف بعده.
الجزء F — ثلاثة جداول، ثم GROUP BY
اربط أولًا، وتحقّق، ثم جمّع:
three = q("""
SELECT p.category, o.region_code, ROUND(SUM(oi.quantity * oi.unit_price_paid), 2) AS revenue
FROM order_items AS oi
INNER JOIN orders AS o ON o.order_id = oi.order_id
INNER JOIN products AS p ON p.product_id = oi.product_id
WHERE o.region_code = 'HAR'
GROUP BY p.category, o.region_code
ORDER BY revenue DESC
""")
print(three.to_string(index=False))
category region_code revenue
Drinks HAR 2198.21
Pantry HAR 1822.04
Fresh HAR 434.22
Household HAR 173.85
تبني قاعدة البيانات أولًا الصفوف المربوطة (صف لكل سطر، مع منطقة الطلب وفئة المنتج)، ثم تصفّي، ثم تجمّع. طابِق: يجب أن يساوي إجمالي الإيرادات المجمّعة الإجمالي على مستوى الأسطر للتصفية نفسها، محسوبًا دون الربط بالمنتجات. فإن كان أكبر فقد ضاعف أحد الروابط الصفوف؛ وإن كان أصغر فقد أسقط أحدها صفوفًا.
اختياري (SQLite 3.39+): الأمران RIGHT JOIN وFULL OUTER JOIN موجودان في إصدارات SQLite الأحدث. وأي RIGHT JOIN يمكن كتابته على أنه LEFT JOIN مع تبديل الجدولين، وهذا يعمل على كل الإصدارات، ولذلك لا تستخدم هذه الدورة إلا LEFT JOIN.
الجزء G — أعد إنتاج الربط بـ merge(validate=...) في pandas
الفحوص نفسها التي استخدمتها في L28 تؤكد ربط SQL بشكل مستقل:
oi = q("SELECT * FROM order_items")
o = q("SELECT order_id, region_code FROM orders")
m = oi.merge(o, on="order_id", how="inner", validate="many_to_one")
print(len(m), "rows; pandas confirms order_id is unique in orders (validate passed)")
try:
oi.merge(q("SELECT * FROM product_costs"), on="product_id", validate="many_to_one")
except Exception as e:
print(type(e).__name__, "-", str(e)[:60])
2165 rows; pandas confirms order_id is unique in orders (validate passed)
MergeError - Merge keys are not unique in right dataset; not a many-to-on
الخيار validate="many_to_one" يُظهر خطأ عندما لا يكون المفتاح الأيمن فريدًا، وهذا بالضبط تضاعف الصفوف الذي رأيته في الجزء E.
5. مثال محلول
Cedar Lane (نشاط خيالي)، بيانات مصطنعة. يسأل مسؤول العمليات: "How many orders did each pickup point handle in the third quarter of 2026 (July–September), and how much revenue did they bring? Include every pickup point, even if it had none." (أي: "كم طلبًا تعاملت معه كل نقطة استلام في الربع الثالث من 2026 (يوليو–سبتمبر)، وكم إيرادات حققت؟ ضمّن كل نقاط الاستلام، حتى التي لم يكن لها طلبات.")
الخطوة 1 — المفاتيح والعلاقات. العمود pickup_points.pickup_id فريد (5 صفوف) ← علاقة واحد لمتعدد مع orders.pickup_id. والعمود orders.order_id فريد ← علاقة واحد لمتعدد مع order_items. والطلبات الإلكترونية ليس لها نقطة استلام (pickup_id هو NULL)؛ فهي خارج هذا السؤال.
الخطوة 2 — توقّع. صف لكل نقطة استلام (5 صفوف)، لأننا نجمّع حسبها، وLEFT JOIN يُبقي كل نقطة.
الخطوة 3 — الاستعلام (ربطان LEFT JOIN، وشرط التاريخ في ON).
pickup = q("""
SELECT p.pickup_id, p.pickup_name,
COUNT(DISTINCT o.order_id) AS orders,
ROUND(COALESCE(SUM(oi.quantity * oi.unit_price_paid), 0), 2) AS revenue
FROM pickup_points AS p
LEFT JOIN orders AS o
ON o.pickup_id = p.pickup_id
AND o.order_date >= '2026-07-01' AND o.order_date < '2026-10-01'
LEFT JOIN order_items AS oi ON oi.order_id = o.order_id
GROUP BY p.pickup_id, p.pickup_name
ORDER BY revenue DESC
""")
print(pickup.to_string(index=False))
pickup_id pickup_name orders revenue
PK-HAR Harbor shop counter 65 1385.42
PK-NOR North shop counter 53 944.29
PK-CEN Central shop counter 53 929.80
PK-STA Station shop counter 19 291.49
PK-L7 Station parcel locker 7 12 207.80
الدالة COUNT(DISTINCT o.order_id) تعدّ الطلبات مع أن الربط الثاني ينشئ صفًا لكل سطر. والدالة COALESCE(SUM(...), 0) تُظهر إيرادات 0 لنقطة ليس لها طلبات؛ والقيمة الافتراضية صحيحة هنا، لأن "لا طلبات" تعني فعلًا "لا إيرادات" (قارن بـ L46، حيث كانت القيمة الافتراضية ستُخفي أمرًا مجهولًا).
الخطوة 4 — طابِق مع جداول لا تحتاج إلى ربط بنقاط الاستلام.
check = q("""
SELECT COUNT(DISTINCT o.order_id) AS orders, ROUND(SUM(oi.quantity * oi.unit_price_paid), 2) AS revenue
FROM orders AS o
LEFT JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE o.pickup_id IS NOT NULL AND o.order_date >= '2026-07-01' AND o.order_date < '2026-10-01'
""")
print(check.to_string(index=False))
print("match:", pickup["orders"].sum() == check["orders"].iloc[0], round(pickup["revenue"].sum(), 2) == check["revenue"].iloc[0])
orders revenue
202 3758.8
match: True True
الخطوة 5 — سجل الربط.
المفاتيح:
pickup_points.pickup_idفريد (5) ←orders.pickup_id؛ وorders.order_idفريد ←order_items.order_id(متعدد). النتيجة: 5 صفوف كما توقعنا. والإجماليات تساوي طلبات الاستلام في الربع الثالث محسوبةً دون جدول نقاط الاستلام. ملاحظة: الطلبات التي ليس لها أسطر (أُلغيت قبل التجهيز) تُحتسب طلبات بإيرادات 0.
6. شرح بصري
orders (1000) customers (260) INNER JOIN → 999 rows (orphan O-… dropped)
┌────────┬──────────┐ ┌──────────┬─────────┐ LEFT JOIN → 1000 rows (orphan kept, c.* = NULL)
│O-… │C017 │──────▶ │C017 │Regular │ ANTI-JOIN → LEFT JOIN … WHERE c.customer_id IS NULL
│O-… │C999 ? │──╳ │… │… │
└────────┴──────────┘ └──────────┴─────────┘
FAN-OUT: order_items (2165) ⋈ product_costs ON product_id only → each product has 2 cost rows → 4330 rows → revenue ×2
LEFT JOIN + condition on the right table: … ON key AND o.date in March → unmatched kept (0)
… ON key WHERE o.date in March → unmatched removed (inner join!)
CHECK EVERY JOIN: key unique where it should be? │ predicted rows = actual rows │ Σ after join = Σ before join
وصف الرسم: في الأعلى يُربط جدول orders ذو الألف صف بجدول customers؛ وأحد الطلبات يشير إلى عميل غير موجود. الربط الداخلي يعطي 999 صفًا ويُسقطه؛ والربط الأيسر يُبقي الألف كلها مع أعمدة عميل NULL؛ والربط العكسي، وهو ربط أيسر يُبقي الصفوف التي يكون فيها مفتاح العميل NULL، يجده. وفي الوسط تضاعف الصفوف: عناصر الطلبات المربوطة بتكاليف المنتجات على رقم المنتج وحده تطابق صفّي تكلفة لكل منتج، فتتضاعف الصفوف والإيرادات. وتحت ذلك ربط أيسر بشرط على الجدول الأيمن: في ON تبقى الصفوف غير المطابقة بقيمة صفر؛ وفي WHERE تُحذف ويتصرف الربط كربط داخلي. وفي الأسفل الفحوص الثلاثة لكل ربط: هل المفتاح فريد حيث يجب أن يكون، وهل عدد الصفوف الفعلي يساوي المتوقع، وهل الإجمالي بعد الربط يساوي الإجمالي قبله. (نصوص الرسم: orphan dropped = اليتيم أُسقط؛ orphan kept = اليتيم بقي؛ FAN-OUT = تضاعف الصفوف؛ unmatched kept = غير المطابق بقي؛ unmatched removed (inner join!) = غير المطابق حُذف (ربط داخلي!)؛ CHECK EVERY JOIN = افحص كل ربط.)
7. تدريب موجَّه
افتح L47_starter.ipynb.
- تحقّق من تفرّد المفاتيح:
customers.customer_idوorders.order_idوproducts.product_id(الصفوف مقابل القيم المختلفة). - توقّع، ثم اربط
ordersبـcustomers(INNER). وسجّل عدد الصفوف. ثم نفّذه كـ LEFT JOIN واشرح الفرق. - توقّع، ثم اربط
order_itemsبـordersوبـproducts(ربطان INNER JOIN). وسجّل عدد الصفوف بعد كل ربط. - قارن
SUM(quantity * unit_price_paid)بعد ربط الجداول الثلاثة بالمجموع نفسه علىorder_itemsوحده. - اكتب سجل ربط (join log): لكل ربط، المفتاح، والعلاقة، وعدد الصفوف المتوقع والفعلي.
- شغّل Restart Kernel and Run All Cells….
8. تدريب مستقل
إيرادات الفئات والمنتجات التي لم تُبع. يطلب المسؤول التجاري شيئين:
"Revenue by product category and region for the whole year, and a list of products that have never sold."
وبالعربية: "الإيرادات حسب فئة المنتج والمنطقة للسنة كلها، وقائمة بالمنتجات التي لم تُبع قط."
- قبل كتابة أي ربط، تحقّق من المفاتيح التي ستستخدمها، وتوقّع عدد صفوف كل ربط.
- اكتب استعلام الإيرادات (صف لكل فئة ومنطقة).
- اكتب استعلام المنتجات التي لم تُبع قط (ربط عكسي) مع رقم المنتج واسمه وفئته وتاريخ طرحه.
- أثبت نتائجك: إجمالي جدول الإيرادات يساوي الإجمالي على مستوى الأسطر؛ وقائمة المنتجات التي لم تُبع مؤكَّدة بطريقة ثانية (مثلًا بمنطق في pandas لا يستخدم
NOT EXISTS: أرقام المنتجات ناقص أرقام المنتجات المبيعة). - وثّق فحوص المفاتيح التي أجريتها ونتائجها.
اعمل في L47_starter.ipynb (الجزء 2).
9. اختبار الفهم (Knowledge Check) — عمليات الربط
Q1. العمود orders.order_id فريد، والعمود order_items.order_id مكرّر. ما العلاقة من orders إلى order_items؟
- A) لا علاقة
- B) واحد لواحد
- C) واحد لمتعدد
- D) متعدد لمتعدد
Q2. تربط 2,000 سطر طلب بـ orders على order_id (فريد في orders). مع INNER JOIN، كم صفًا تتوقع إذا كان لكل سطر طلب؟
- A) يعتمد على عدد العملاء
- B) 1,000
- C) 4,000
- D) 2,000
Q3. أي استعلام يجد الطلبات التي عميلها غير موجود في customers؟
- A)
SELECT … FROM orders WHERE customer_id = NULL - B)
SELECT … FROM orders o INNER JOIN customers c ON c.customer_id = o.customer_id WHERE c.customer_id IS NULL - C)
SELECT … FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id - D)
SELECT … FROM orders o LEFT JOIN customers c ON c.customer_id = o.customer_id WHERE c.customer_id IS NULL
Q4. ربط LEFT JOIN من pickup_points إلى orders فيه WHERE o.order_date >= '2026-03-01'. ماذا يحدث لنقاط الاستلام التي ليس لها طلبات في مارس؟
- A) تسبّب خطأ
- B) تتكرر
- C) تبقى بقيمة 0
- D) تُحذف، لأن
o.order_dateهو NULL لها والشرط في WHERE ليس صحيحًا
Q5. بعد ربط order_items (2,165 صفًا) بجدول تكاليف على product_id، تحصل على 4,330 صفًا. ماذا حدث؟
- A) INNER JOIN يضاعف الصفوف دائمًا
- B) تضاعف الصفوف: لكل منتج صفّان في جدول التكاليف
- C) الربط صحيح
- D) جدول التكاليف فارغ
Q6. لماذا يُظهر SELECT COUNT(*) … FROM pickup_points p LEFT JOIN orders o … GROUP BY p.pickup_id القيمة 1 لنقطة ليس لها طلبات؟
- A) النقطة لها طلب واحد فعلًا
- B) COUNT(*) يعدّ الصف الواحد الذي يُبقيه LEFT JOIN للنقطة غير المطابقة؛ استخدم
COUNT(o.order_id) - C) GROUP BY يضيف صفًا
- D) في SQLite خلل برمجي
Q7. في SQLite، هل تمنع REFERENCES customers(customer_id) في تعريف الجدول أن يشير طلب إلى عميل غير موجود؟
- A) فقط إذا فُعّلت المفاتيح الأجنبية للاتصال (
PRAGMA foreign_keys = ON) - B) فقط للمفاتيح النصية TEXT
- C) دائمًا
- D) أبدًا، فالمفاتيح الأجنبية غير موجودة في SQLite
Q8. بعد ربط ثلاثة جداول وGROUP BY، كان إجمالي الإيرادات المجمّعة أعلى من الإجمالي على مستوى الأسطر. ما السبب الأرجح؟
- A) التقريب
- B) قيم NULL
- C) أحد الروابط ضاعف الصفوف (مفتاح غير فريد في أحد الجانبين)
- D) INNER JOIN أسقط صفوفًا
10. الذكاء الاصطناعي في التطبيق (اختياري، وفي حدود)
الهدف: أن تطلب من مساعد ذكاء اصطناعي ربطًا بين ثلاثة جداول، وتتحقق منه قبل أن تثق به.
- أعطه قوائم أعمدة
order_itemsوordersوproductsواسأل:
"Write a SQLite query for revenue (quantity × unit_price_paid) by product category and month (from order_date)."
وبالعربية: "اكتب استعلام SQLite للإيرادات (quantity × unit_price_paid) حسب فئة المنتج والشهر (من order_date)."
- قبل تشغيله، اكتب المفاتيح التي يجب أن يستخدمها، وعدد الصفوف المربوطة (قبل التجميع).
- شغّله واتّبع القسم 11.
الحدود:
- استخدام الذكاء الاصطناعي اختياري.
- شارك أسماء الأعمدة وأسماء الجداول، لا البيانات.
- الذكاء الاصطناعي لا يعرف أي المفاتيح فريد في بياناتك. وتضاعف الصفوف والصفوف الساقطة مسؤوليتك أنت أن تفحصها.
11. التحقق من مخرجات الذكاء الاصطناعي
- المفاتيح: هل شروط الربط على الأعمدة الصحيحة (
order_idوproduct_id)؟ وهل استخدم أي جدول آخر؟ - عدد الصفوف: شغّل جزء
FROM … JOIN …من استعلامه معCOUNT(*). هل يطابق توقعك؟ - الإجمالي: هل مجموع إيراداته المجمّعة يساوي الإجمالي على مستوى الأسطر؟
- pandas: أعد إنتاج ربط واحد بـ
merge(validate=...). - دوّن شيئًا واحدًا أصاب فيه، وشيئًا واحدًا فحصته أنت ولم يذكره.
12. منظور الأعمال
تقارير الإيرادات عمليات ربط. جدول إيرادات الفئات حسب المنطقة، وملخّص شرائح العملاء، وجدول ترتيب نقاط الاستلام: كل منها يجمع عدة جداول. وعندما يضاعف ربطٌ الصفوف، تبدو إيرادات منطقة أعلى بنسبة 20% وتُصرف عليها مكافأة؛ وعندما يُسقط ربط داخلي الصفوف اليتيمة، لا تُطابَق فاتورة مورّد أبدًا ولا ينتبه أحد.
والعادة المهنية هي سجل ربط بجانب كل تقرير: المفاتيح، والعلاقة، وعدد الصفوف المتوقع والفعلي، ومطابقة الإجماليات. يستغرق دقائق، وهو أول ما يبحث عنه المراجع.
13. مهمة عمل — جدول إيرادات الشرائح
نشاط خيالي: Cedar Lane. يريد مسؤول التسويق الإيرادات وعدد الطلبات حسب شريحة العملاء للسنة كلها.
- اكتب الاستعلام (الأسطر ← الطلبات ← العملاء). وقرّر ماذا يحدث للطلب اليتيم وللعملاء الذين لم تُسجَّل لهم شريحة؛ واعرض الحالتين كسطرين ظاهرين (مثل "unknown customer" أي عميل غير معروف، و"segment not recorded" أي الشريحة غير مسجّلة) بدلًا من تركهما يختفيان.
- طابِق الجدول مع إجمالي إيرادات كل الأسطر.
- اكتب مذكرة من ثلاثة أسطر لمسؤول التسويق، تتضمن كيف يُقرأ السطران الخاصان.
14. تحدٍّ قصير — التقرير الذي كبر
نشاط خيالي: Cedar Lane. أضاف زميل تكاليف الوحدات إلى تقرير إيرادات:
print(q("""
SELECT ROUND(SUM(oi.quantity * oi.unit_price_paid), 2) AS revenue,
ROUND(SUM(oi.quantity * pc.unit_cost), 2) AS cost
FROM order_items AS oi
JOIN product_costs AS pc ON pc.product_id = oi.product_id
""").to_string(index=False))
- قبل تشغيله، توقّع ما الخطأ في الرقمين كليهما.
- شغّله، ثم احسب الإيرادات الحقيقية من
order_itemsوحده. واشرح الفرق بأعداد الصفوف. - صِف بالكلمات ما يجب أن يطابقه الربط الصحيح بـ
product_costs(ستكتبه في L48). - اكتب جملة واحدة يحتفظ بها زميلك كقاعدة.
سلّم عملك قبل أن تتلقى الملاحظات. لا يوجد حل منشور هنا.
15. المصادر
مطلوب (حوالي 30 دقيقة):
- SQLite: SELECT، القسم الخاص بجزء FROM وعوامل الربط. https://www.sqlite.org/lang_select.html (فُتح في 20 أكتوبر 2026.)
موصى به:
- SQLite: Quirks, Caveats, and Gotchas In SQLite، القسم الخاص بتعطيل المفاتيح الأجنبية افتراضيًا. https://www.sqlite.org/quirks.html
- SQLite Release 3.39.0 (2022-06-25)، الإصدار الذي أضاف RIGHT JOIN وFULL OUTER JOIN (اختياريان في هذه الدورة). https://sqlite.org/releaselog/3_39_0.html
متابعة مفتوحة (D.World، SN03): لم يُختبر بعد تثبيت إصدار SQLite المرفق مع Python على Windows وmacOS وLinux لهذه الدورة. ولا يستخدم هذا الدرس إلا INNER JOIN وLEFT JOIN، وهما يعملان على كل الإصدارات.
16. شخصية تتابعها / قدوة
D. Richard Hipp (اختارته D.World للدروس L47–L50).
- حقيقة مُتحقَّق منها: طوّر الدكتور D. Richard Hipp قاعدة SQLite في الأصل وأصدرها عام 2000 (بيان صحفي من SQLite Consortium، مذكور في خريطة D.World للمرحلة 3).
- لماذا توصي به D.World: سجل إصدارات SQLite يوثّق بالضبط متى وصلت كل ميزة (مثل RIGHT JOIN في 3.39.0)، وبهذا تقرر ما الآمن استخدامه.
- ملاحظة صادقة: لا نذكر إلا الحقيقة المذكورة أعلاه.
- جرّب هذا: افتح سجل الإصدار 3.39.0 وابحث عن السطر الخاص بـ RIGHT JOIN وFULL OUTER JOIN.
17. المصطلحات
| المصطلح | معناه في هذا الدرس |
|---|---|
| INNER JOIN (الربط الداخلي) | لا يُبقي إلا أزواج الصفوف المتطابقة على المفتاح |
| LEFT JOIN (الربط الأيسر) | يُبقي كل صف في الجدول الأيسر؛ وأعمدة الجدول الأيمن غير المطابقة تكون NULL |
| الربط العكسي (Anti-join) | يجد صفوف الجدول الأيسر التي لا مطابق لها: LEFT JOIN … WHERE right.key IS NULL |
| تضاعف الصفوف (Fan-out) | صفوف تضاعفت بسبب ربط لأن المفتاح غير فريد في أحد الجانبين |
| علاقة التعدد (Cardinality) | كم صفًا في كل جانب قد يشترك في قيمة مفتاح: واحد لواحد، واحد لمتعدد، متعدد لمتعدد |
| الاسم البديل للجدول (Table alias) | اسم قصير لجدول في استعلام، مثل FROM orders AS o |
للمراجعة: الربط والدمج، وvalidate= وindicator= (L28) · المفاتيح الأساسية والأجنبية (L43).
18. تأمّل
- اشرح لزميل، دون SQL، لماذا قد يجعل الربطُ الإيراداتِ ترتفع.
- متى يكون INNER JOIN الخيار الصحيح، ومتى LEFT JOIN؟
- ماذا سيحتوي سجل الربط الخاص بك من الآن فصاعدًا؟
19. الخطوة التالية
أصبحت الآن قادرًا على ربط الجداول وإثبات النتيجة. والتالي:
- L48 — الاستعلامات الفرعية وتعبيرات CTE (Subqueries & CTEs). الأسئلة متعددة الخطوات (مثل التكلفة السارية في تاريخ كل طلب) تصبح خطوات مقروءة وقابلة للاختبار.
20. أدلة التقييم
| الدليل | ما الذي يُظهره | كيف يُقيَّم |
|---|---|---|
| إيرادات الفئة × المنطقة + قائمة ما لم يُبع + فحوص المفاتيح (التدريب المستقل) | يتوقع أعداد الصفوف؛ وعمليات ربط صحيحة؛ وربط عكسي؛ وإجماليات متطابقة؛ وفحوص مفاتيح موثّقة | دليل التقييم L47-IP |
| التدريب الموجَّه (سجل الربط) | تفرّد المفاتيح؛ والأعداد المتوقعة مقابل الفعلية؛ والإجماليات قبل وبعد | دليل التقييم L47-GP |
| اختبار الفهم (Q1–Q8) | علاقات التعدد، وINNER مقابل LEFT، والربط العكسي، وON مقابل WHERE، وتضاعف الصفوف، والمفاتيح الأجنبية | 7 إجابات صحيحة أو أكثر من 8 |
| مهمة العمل والتحدي القصير | إظهار السطور الخاصة؛ وشرح تضاعف الصفوف | دليلا التقييم L47-WT وL47-MC |
المراجع
- SQLite. SELECT (FROM clause, joins). https://www.sqlite.org/lang_select.html (SQ-SEL). يدعم: INNER JOIN وLEFT JOIN وUNION ALL.
- SQLite. Release 3.39.0. https://sqlite.org/releaselog/3_39_0.html (SQ-REL339). يدعم: توفّر RIGHT JOIN وFULL OUTER JOIN.
- SQLite. Quirks, Caveats, and Gotchas In SQLite. https://www.sqlite.org/quirks.html (SQ-QRK). يدعم: تعطيل المفاتيح الأجنبية افتراضيًا.
- pandas development team. pandas.DataFrame.merge (validate=). استُخدم كما رُوجع في L28.
- D.World Stage 3 Detailed Lesson Map v0.9 (صف L47؛ SN03؛ حقيقة القدوة). وثيقة داخلية.
L48 · Subqueries & CTEs: Readable Multi-Step Queries Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
L49 · Window Functions: Ranking, Running Totals & LAG Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
L50 · Time Analysis in SQL: Periods, Gaps & Period-over-Period Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
L51 · Cohorts & Repeatable, Tested Analytical Queries Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
U12 Exit = Project 3 — SQL Business Analysis Membersللمشتركين
The exit brief is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
تكليف نهاية الوحدة ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
U13 · Statistics for Data Scienceالإحصاء لعلم البيانات
Stage 3المرحلة 3Free: the first lesson. The other lessons, the exit brief and the practice files are for Starter and Membership.
مجاني: الدرس الأول. باقي الدروس وتكليف نهاية الوحدة وملفات التمرين لأصحاب باقة البداية والاشتراك.
Download U13 practice files (.zip, 128 KB)حمّل ملفات تمرين U13 (zip، 128 KB)
Unzip into a short folder such as D:\DWorld on Windows. The zip has only learner files: no answers.
فك الضغط في مجلد قصير مثل D:\DWorld على ويندوز. الملف يحتوي ملفات المتعلم فقط بدون إجابات.
L52 · Describing Data: Center, Spread & ShapeL52 · وصف البيانات: المركز والانتشار والشكل Freeمجاني
L52 — Describing Data: Center, Spread & Shape
Learner file. Answers and model solutions are in a separate instructor-only file. Version: U13 Production v2 · Content date: 20 October 2026. Time estimates are planning estimates until learners have tested them. The code in this lesson was run with the course setup from L21 (Python 3.13, pandas 3.0 and NumPy 2.5; SciPy 1.18 and Matplotlib 3.11 where used) on Linux and on Windows 11 on 4 November 2026 (see the Code Test Report).
All businesses are fictional and all data is synthetic. Figures are drawn in your notebook; this page shows the numbers printed by the same code.
1. Lesson Identity
| Field | Value |
|---|---|
| Title | Describing Data: Center, Spread & Shape |
| Stage | Stage 3 — Data Analysis |
| Unit | U13 — Statistics for Data Science |
| Lesson ID | L52 |
| Skills | Installing SciPy and Matplotlib; mean vs median; variance and standard deviation (ddof); percentiles, quartiles, IQR; skew; outliers' effect on summaries; histogram and box plot with fig, ax = plt.subplots() |
| Difficulty | Intermediate |
| Estimated time | About 2.5 hours (planning estimate, unvalidated; includes the install and version check) |
| Assessment level | Practice |
| Prerequisites | L21–L23 (NumPy aggregation), L25 (describe()), L38 (outliers as data). |
Time breakdown (planning estimate):
- Setup (install SciPy and Matplotlib, version check): 20 min.
- Concept and worked example: 50 min.
- Guided practice: 25 min.
- Independent practice: 25 min.
- Knowledge check: 10 min.
- AI in Practice and Verification: 5 min (optional).
- Workplace task and mini challenge: 15 min.
What you need: your L21 environment, internet access once for the install, and practice_files (L52_starter.ipynb; u13_data/orders_year.csv).
2. Learning Objective
By the end of this lesson, you will be able to:
- summarise a numeric variable with mean, median, standard deviation, IQR and percentiles;
- show its shape with a histogram and a box plot;
- choose and justify the summary that fits a skewed distribution.
D.World approved objective (verbatim): Summarise a numeric variable with mean, median, standard deviation, IQR and percentiles, show its shape with a histogram and box plot, and explain which summary fits a skewed distribution.
3. Why It Matters
Cedar Lane's managers know one number about orders: the average basket value. But a few very large orders pull the average up. If the average is 23.58, is a "typical" order around 23? Most orders are smaller than that. A store planning promotions around "the average customer" plans for someone who rarely appears.
Statistics starts with describing data honestly: where the middle is, how spread out the values are, and what shape the distribution has. The right summary depends on the shape, and the shape is something you see in a plot before you trust any number.
Key principle: look at the shape first, then choose the summary that describes it; for a skewed variable, the median and percentiles usually describe the typical case better than the mean.
4. Concept
Part A — Setup: SciPy and Matplotlib
U13 adds two libraries to your Stage 2 environment: SciPy (statistical functions) and Matplotlib (plots). With your virtual environment activated (as in L21), install them once:
python -m pip install "scipy==1.18.*" "matplotlib==3.11.*"
Then check the versions in a notebook:
import sys
import matplotlib
import numpy as np
import pandas as pd
import scipy
import matplotlib.pyplot as plt
print("Python ", sys.version.split()[0])
print("NumPy ", np.__version__)
print("pandas ", pd.__version__)
print("SciPy ", scipy.__version__)
print("Matplotlib", matplotlib.__version__)
orders = pd.read_csv("u13_data/orders_year.csv")
print(orders.shape)
Python 3.13.16
NumPy 2.5.3
pandas 3.0.5
SciPy 1.18.1
Matplotlib 3.11.2
(2400, 6)
Your versions should start with 1.18 and 3.11 (this page was produced with slightly older versions; the functions used behave the same). If the install fails, see the L21 troubleshooting notes and record the error; do not continue with a mix of environments.
Part B — Center: mean and median
The mean is the sum divided by the count. The median is the middle value when the values are sorted (half are below it, half above).
bv = orders["basket_value"]
print("mean ", round(bv.mean(), 2))
print("median", round(bv.median(), 2))
print("share of orders below the mean:", round((bv < bv.mean()).mean(), 3))
mean 23.58
median 19.47
share of orders below the mean: 0.624
More than half of all orders are below the mean. A few large orders pull the mean up; the median does not move with them.
Part C — Spread: variance, standard deviation, percentiles, IQR
Spread describes how far values are from the middle.
- Variance is the average squared distance from the mean; the standard deviation (SD) is its square root, in the same units as the data.
- For a sample, the variance divides by n − 1 instead of n ("ddof = 1", delta degrees of freedom). pandas uses
ddof=1by default; NumPy usesddof=0by default. Always say which you used. - A percentile is the value below which a given share of the data lies: the 90th percentile has 90% of values below it. The quartiles are the 25th, 50th (median) and 75th percentiles. The IQR (interquartile range) is Q3 − Q1: the range of the middle half.
print("SD pandas (ddof=1):", round(bv.std(), 4))
print("SD NumPy (ddof=0):", round(np.std(bv.to_numpy()), 4))
print("SD NumPy (ddof=1):", round(np.std(bv.to_numpy(), ddof=1), 4))
q1, q3 = bv.quantile([0.25, 0.75])
print("Q1", round(q1, 2), "| Q3", round(q3, 2), "| IQR", round(q3 - q1, 2))
print("P10, P90:", bv.quantile([0.10, 0.90]).round(2).tolist())
SD pandas (ddof=1): 16.8777
SD NumPy (ddof=0): 16.8742
SD NumPy (ddof=1): 16.8777
Q1 12.92 | Q3 29.02 | IQR 16.1
P10, P90: [8.94, 42.43]
The two SD values differ slightly because of n versus n − 1. With 2,400 values the difference is tiny; with 10 values it is not.
Part D — Shape: skew and outliers
A distribution is skewed when one tail is longer than the other. Order values are right-skewed: many small and medium orders, a long tail of large ones. Signs of right skew: mean > median, and the distance from the median to the 90th percentile is larger than from the 10th percentile to the median.
from scipy import stats
print("median - P10:", round(bv.median() - bv.quantile(0.10), 2))
print("P90 - median:", round(bv.quantile(0.90) - bv.median(), 2))
print("skewness (SciPy):", round(stats.skew(bv), 2))
median - P10: 10.53
P90 - median: 22.96
skewness (SciPy): 3.4
A skewness near 0 means roughly symmetric; positive means a longer right tail. Describe skew in words in reports ("most orders are between 13 and 29; a few go above 100").
Outliers move the mean and SD a lot, and the median and IQR very little. Add one extreme order and compare:
with_extreme = pd.concat([bv, pd.Series([2500.0])], ignore_index=True)
for name, s in [("original", bv), ("plus one 2,500 order", with_extreme)]:
print(f"{name:22} mean {s.mean():7.2f} median {s.median():6.2f} SD {s.std():7.2f} IQR {s.quantile(0.75) - s.quantile(0.25):6.2f}")
original mean 23.58 median 19.47 SD 16.88 IQR 16.10
plus one 2,500 order mean 24.61 median 19.47 SD 53.28 IQR 16.12
One order out of 2,401 changed the mean by about one euro and more than tripled the SD, while the median and IQR hardly moved. In this lesson you describe outliers; whether to keep, cap or investigate them is a data decision you practised in U09 (L38).
Part E — Plots: histogram and box plot with fig, ax
Matplotlib's fig, ax = plt.subplots() creates a figure (the whole picture) and an axes (one plot area). You draw on ax. This is the interface U15 builds on.
fig, ax = plt.subplots(figsize=(7, 3.5))
counts, edges, _ = ax.hist(bv, bins=40)
ax.set_xlabel("Basket value (€)")
ax.set_ylabel("Number of orders")
ax.set_title("Basket value per order, synthetic year")
print("bins:", len(counts), "| counts add up to rows:", int(counts.sum()) == len(bv))
print("tallest bin from", round(edges[counts.argmax()], 1), "to", round(edges[counts.argmax() + 1], 1))
bins: 40 | counts add up to rows: True
tallest bin from 14.0 to 20.2
A histogram splits the range into bins and counts the values in each. The bin choice changes the picture: too few bins hide the shape, too many make it noisy. Try 10, 40 and 100 bins and keep the one that shows the shape honestly. The counts must add up to the number of rows.
A box plot shows the median (line), the quartiles (box), and "whiskers" up to 1.5 × IQR beyond the box; points beyond are drawn individually:
fig, ax = plt.subplots(figsize=(7, 3))
bp = ax.boxplot([orders.loc[orders["region_code"] == r, "basket_value"] for r in ["CEN", "HAR", "NOR", "STA", "WEB"]],
tick_labels=["CEN", "HAR", "NOR", "STA", "WEB"], orientation="horizontal")
ax.set_xlabel("Basket value (€)")
print("points drawn beyond the whiskers:", sum(len(f.get_xdata()) for f in bp["fliers"]))
points drawn beyond the whiskers: 107
Box plots are good for comparing groups side by side. The individual points are values far from the middle, not "errors".
Part F — describe(), reread
describe() (L25) prints most of these summaries at once. Now you can read every line:
print(bv.describe().round(2).to_string())
count 2400.00
mean 23.58
std 16.88
min 1.47
25% 12.92
50% 19.47
75% 29.02
max 251.25
std uses ddof = 1; 25%, 50%, 75% are the quartiles; min and max show the extremes. Compare mean and 50% first: if they differ a lot, the distribution is skewed.
5. Worked Example
Cedar Lane (fictional), synthetic data. The logistics manager asks: "How many days does a delivery usually take? I've been quoting customers the average."
Step 1 — Look at the shape. delivery_days is a whole number of days.
dd = orders["delivery_days"]
print(dd.value_counts().sort_index().to_string())
delivery_days
1 472
2 976
3 581
4 239
5 79
6 32
7 12
8 6
9 1
10 2
Most deliveries take 1–3 days; a thin tail goes up to 10. Right-skewed.
Step 2 — Summaries.
print("mean", round(dd.mean(), 2), "| median", dd.median(), "| P90", dd.quantile(0.90), "| P95", dd.quantile(0.95))
print("share delivered within 3 days:", round((dd <= 3).mean(), 3))
mean 2.45 | median 2.0 | P90 4.0 | P95 5.0
share delivered within 3 days: 0.845
Step 3 — Choose the summary for the purpose. For a promise to a customer, a typical value is not enough: what matters is how often the promise is kept. "Within 3 days" covers about 85% of orders; "within 4 days" covers more. The mean (2.45 days) is not a promise anyone can keep.
Step 4 — Plot.
fig, ax = plt.subplots(figsize=(6, 3))
counts, edges, _ = ax.hist(dd, bins=range(1, 12), align="left", rwidth=0.9)
ax.set_xlabel("Delivery days"); ax.set_ylabel("Orders"); ax.set_xticks(range(1, 11))
print("histogram counts:", [int(c) for c in counts], "| total:", int(counts.sum()))
histogram counts: [472, 976, 581, 239, 79, 32, 12, 6, 1, 2] | total: 2400
With whole-day values, one bin per day (bins=range(1, 12)) shows the shape exactly.
Step 5 — Profile for the manager: "Half of deliveries arrive within 2 days and about 85% within 3 days; about 1 in 20 takes 5 days or more. Quote '2–3 days' with 'up to 5 days' as the honest upper limit, rather than the average of 2.45 days."
6. Visual Explanation
RIGHT-SKEWED (basket value) BOX PLOT
count ├──────[ Q1 │median│ Q3 ]──────┤ · · ·
██ whisker IQR box whisker points beyond
███ (middle half) 1.5 × IQR
█████▄
███████▄▂ long right tail MEAN is pulled toward the tail; MEDIAN stays in the middle
█████████▆▄▃▂▁▁ ▁ ▁ · ADD ONE EXTREME VALUE → mean, SD jump; median, IQR barely move
──────────────────────────────────▶ value
↑median ↑mean SD: pandas ddof=1, NumPy ddof=0 by default — say which
Alt text: On the left, a histogram of a right-skewed variable: tall bars at low values and a long thin tail to the right, with the median marked to the left of the mean. On the right, a horizontal box plot: whiskers, a box from the first to the third quartile with the median inside, and individual points beyond 1.5 times the IQR. Notes: the mean is pulled toward the tail while the median stays in the middle; adding one extreme value makes the mean and standard deviation jump while the median and IQR barely move; pandas uses ddof equal to one and NumPy ddof equal to zero by default.
7. Guided Practice
Open L52_starter.ipynb.
- Run the setup cell and record your versions. Are SciPy and Matplotlib the expected versions?
- Basket values: mean, median, SD (ddof = 1), Q1, Q3, IQR, P90. Draw a histogram (try 10, 40 and 100 bins; keep one) and check that its counts add up to the rows.
- Delivery days: the same summaries and a one-bin-per-day histogram.
- Add one extreme basket value of your choice (for example 3,000) to a copy of the basket values. Compare all summaries before and after in one small table.
- Recompute the SD and IQR of basket values with NumPy (
np.std(..., ddof=1),np.percentile) and reconcile with pandas. - Run Restart Kernel and Run All Cells….
8. Independent Practice
A distribution profile per region. The regional managers only know their average basket value.
- For each region, compute the summaries you think a manager needs and draw one comparison plot.
- Decide which summary describes a "typical" order in each region, and justify the choice from the shape.
- Write a profile of 120–180 words for the managers: what a typical order looks like, how spread out orders are, and what the average hides. Use at least one percentile.
- Check your numbers a second way (NumPy, or a manual quantile) for one region.
Work in L52_starter.ipynb (Part 2).
9. Knowledge Check — Describing Data
Q1. A variable has mean 23.6 and median 19.5. What does this most likely tell you?
- A) It is symmetric
- B) The data contain errors
- C) It is right-skewed: a tail of large values pulls the mean up
- D) It is left-skewed
Q2. Which summary changes least when you add one extremely large value?
- A) Maximum
- B) Standard deviation
- C) Mean
- D) Median
Q3. pandas s.std() and NumPy np.std(a) give slightly different numbers on the same data. Why?
- A) pandas uses ddof = 1 (divides by n − 1), NumPy uses ddof = 0 by default
- B) They use different units
- C) NumPy is less precise
- D) pandas ignores the largest value
Q4. What is the IQR?
- A) The 75th percentile minus the 25th percentile
- B) Mean minus median
- C) Maximum minus minimum
- D) Two standard deviations
Q5. What does the 90th percentile of delivery days mean?
- A) 90 days
- B) 90% of deliveries take that many days or fewer
- C) 90% of deliveries take that many days
- D) The average of the slowest 10%
Q6. The counts of a histogram add up to fewer than the number of rows. What should you suspect?
- A) Some values fell outside the bin range (or were missing) and were not counted
- B) The variable is skewed
- C) The bins are too wide
- D) Nothing, histograms round counts
Q7. In a box plot, what are the individual points beyond the whiskers?
- A) The mean
- B) Errors that must be deleted
- C) Missing values
- D) Values more than 1.5 × IQR beyond the box; worth a look, not automatically errors
Q8. A manager wants to promise customers a delivery time that is kept 9 times out of 10. Which summary helps most?
- A) The mean
- B) The standard deviation
- C) The minimum
- D) The 90th percentile
10. AI in Practice (optional, bounded)
Goal: ask an AI assistant which summary suits a right-skewed variable, then check its claim against your plot and numbers.
- Describe the delivery-days variable in words (whole days, 1 to 10, most 1–3) and ask:
"Which summary statistics should I report for a right-skewed delivery-time variable, and why?"
- Compare its answer with your summaries and histogram.
Boundaries:
- AI is optional.
- Share a description and summary numbers, not raw customer data.
- The AI cannot see your plot; a recommendation that ignores the shape you see is wrong for your data.
11. AI Verification
- Shape: does its advice match the shape in your histogram?
- Numbers: compute every summary it recommends. Do they say something useful to the manager?
- ddof: if it gives a formula for SD, does it say n or n − 1?
- Write down one recommendation you accept and one you reject, with the reason.
12. Business Lens
Averages hide the customer you are planning for. A promotion threshold, a delivery promise or a staffing plan built on the mean of a skewed variable is built for a customer who rarely appears. Medians and percentiles describe the typical case and the tail separately, which is exactly what operational decisions need ("most orders", "9 out of 10 deliveries").
Showing the shape (one histogram or box plot) next to the numbers is the fastest way to stop a meeting from arguing about an average.
13. Workplace Task — The Free-Delivery Threshold
Fictional business: Cedar Lane. Marketing proposes free delivery for orders "above the average basket value".
- Compute what share of orders would qualify at the mean, at the median and at the 75th percentile.
- Write a short note (80–120 words) explaining why "above the average" is not "above most orders" for this data, and which threshold you would discuss instead, depending on the goal (reward the largest quarter of orders, or half of all orders).
14. Mini Challenge — The SD That Was Too Small
Fictional business: Cedar Lane. A colleague reports the SD of the ten largest basket values with NumPy and compares it with your pandas value.
top10 = bv.nlargest(10)
print(round(np.std(top10.to_numpy()), 3), round(top10.std(), 3))
- Before running it, predict which number is larger and why.
- Run it and explain the difference for n = 10 compared with n = 2,400.
- Write the NumPy call that matches pandas.
- Write one sentence your colleague can keep as a rule.
Submit before feedback. No solution is provided here.
15. Resources
Required (about 40 min):
- Think Stats, 3rd edition (Allen B. Downey, Green Tea Press; free online with notebooks), the chapters on distributions and summary statistics. https://greenteapress.com/wp/think-stats-3e/ (Opened 20 Oct 2026.)
- Matplotlib: Quick start guide, the sections on figures and axes (
fig, ax = plt.subplots()). https://matplotlib.org/stable/users/explain/quick_start.html
Recommended:
- OpenIntro: Introduction to Modern Statistics (2nd ed.), free PDF and web book: the chapter on exploring numerical data. https://www.openintro.org/book/ims/
- Practical Statistics for Data Scientists, 2nd ed. (Bruce, Bruce, Gedeck; O'Reilly, 2020), chapter 1 (paid book; the free texts above cover the required content). https://www.oreilly.com/library/view/practical-statistics-for/9781492072935/
Reference (install pages):
- PyPI: scipy — https://pypi.org/project/scipy/ · PyPI: matplotlib — https://pypi.org/project/matplotlib/
Open follow-up (D.World, SN06): the install of SciPy 1.18.x and Matplotlib 3.11.x has not yet been tested on Python 3.14 and on Windows/macOS for this course.
16. Person to Follow / Role Model
Allen B. Downey (mapped by D.World for L52–L53).
- Verified fact: author of Think Stats (3rd edition), published free online with runnable notebooks by Green Tea Press (D.World Stage 3 map).
- Why D.World recommends him: his books teach statistics through code and simulation, the same approach U13 uses.
- Honest note: we cite only the fact above.
- Try this: open the Think Stats page and find the chapter that introduces percentiles. How does it define a percentile rank?
17. Glossary
| Term | Meaning in this lesson |
|---|---|
| Mean | Sum divided by count; pulled by extreme values |
| Median | The middle value of the sorted data; half below, half above |
| Standard deviation | Typical distance from the mean, in the data's units (sample SD uses n − 1) |
| Variance | The square of the standard deviation |
| Percentile | The value below which a given share of the data lies |
| IQR | Interquartile range: 75th minus 25th percentile; the range of the middle half |
| Skew | Asymmetry of a distribution; right skew = long tail of large values |
| Histogram | A plot of counts per bin of a numeric variable |
| Box plot | A plot of median, quartiles, whiskers and points far from the middle |
Review: aggregation (L23) · describe() (L25) · outliers (L38).
18. Reflection
- Which summary would you now report first for a skewed variable, and why?
- When does the difference between ddof = 0 and ddof = 1 matter?
- Where in your own work is an average hiding a long tail?
19. Next Step
You can now describe a variable honestly. Next:
- L53 — Probability & Distributions for Data Work. From describing what happened to modelling chance: conditional probabilities and simulation.
20. Assessment Evidence
| Evidence | What it shows | How it is judged |
|---|---|---|
| Environment check + regional distribution profile (Independent Practice) | Versions recorded; summaries chosen from the shape; comparison plot; 120–180-word profile with a percentile; second-route check | Rubric L52-IP |
| Guided practice (baskets, delivery days, extreme value) | Correct summaries, ddof reconciled, histogram counts checked, outlier effect shown | Rubric L52-GP |
| Knowledge check (Q1–Q8) | Center, spread, ddof, percentiles, shape, box plots | 7 or more of 8 correct |
| Workplace task and mini challenge | Threshold reasoning with shares; ddof explained for small n | Rubrics L52-WT, L52-MC |
References
- Downey, A. B. Think Stats, 3rd ed. Green Tea Press. https://greenteapress.com/wp/think-stats-3e/ (TS3). Supports: distributions, summary statistics; role-model fact.
- Matplotlib developers. Quick start guide. https://matplotlib.org/stable/users/explain/quick_start.html (MPL-QS). Supports:
fig, ax = plt.subplots(), hist, boxplot. - OpenIntro. Introduction to Modern Statistics, 2nd ed. https://www.openintro.org/book/ims/ (OI-IMS). Supports: numerical summaries.
- Bruce, P., Bruce, A., Gedeck, P. Practical Statistics for Data Scientists, 2nd ed. O'Reilly, 2020. https://www.oreilly.com/library/view/practical-statistics-for/9781492072935/ (PSDS).
- PyPI: scipy; matplotlib. https://pypi.org/project/scipy/ · https://pypi.org/project/matplotlib/ (PYPI-SC, PYPI-MPL). Supports: install versions.
- D.World Stage 3 Detailed Lesson Map v0.9 (L52 row; SN06; SN04). Internal.
L52 — وصف البيانات: المركز والانتشار والشكل
ملف المتعلّم. الإجابات والحلول النموذجية في ملف منفصل خاص بالمدرّس فقط. الإصدار: U13 Production v2 · تاريخ المحتوى: 20 أكتوبر 2026. تقديرات الوقت تقديرات للتخطيط إلى أن يختبرها المتعلّمون. الكود في هذا الدرس شُغّل باستخدام تجهيزات الدورة من L21 (Python 3.13 وpandas 3.0 وNumPy 2.5؛ وSciPy 1.18 وMatplotlib 3.11 حيث استُخدمتا) على Linux وعلى Windows 11 في 4 نوفمبر 2026 (راجع تقرير اختبار الكود Code Test Report).
كل الأنشطة التجارية خيالية، وكل البيانات مصطنعة. والرسوم تُرسم في دفترك؛ أما هذه الصفحة فتعرض الأرقام التي يطبعها الكود نفسه.
1. بطاقة الدرس
| البند | القيمة |
|---|---|
| العنوان | وصف البيانات: المركز والانتشار والشكل |
| المرحلة | المرحلة 3 — تحليل البيانات |
| الوحدة | U13 — الإحصاء لعلم البيانات |
| رقم الدرس | L52 |
| المهارات | تثبيت SciPy وMatplotlib؛ المتوسط مقابل الوسيط؛ التباين والانحراف المعياري (ddof)؛ المئينات والربيعات والمدى الربيعي (IQR)؛ الالتواء؛ أثر القيم المتطرفة على الملخّصات؛ المدرج التكراري (histogram) والمخطط الصندوقي (box plot) باستخدام fig, ax = plt.subplots() |
| المستوى | متوسط |
| الوقت المتوقع | حوالي ساعتين ونصف (تقدير للتخطيط، غير مُتحقَّق منه؛ ويشمل التثبيت وفحص الإصدارات) |
| مستوى التقييم | تدريب |
| المتطلبات السابقة | L21–L23 (التجميع في NumPy)، وL25 (describe())، وL38 (القيم المتطرفة كبيانات). |
تفصيل الوقت (تقدير للتخطيط):
- التجهيز (تثبيت SciPy وMatplotlib، وفحص الإصدارات): 20 دقيقة.
- المفهوم والمثال المحلول: 50 دقيقة.
- التدريب الموجَّه: 25 دقيقة.
- التدريب المستقل: 25 دقيقة.
- اختبار الفهم: 10 دقائق.
- الذكاء الاصطناعي في التطبيق والتحقق منه: 5 دقائق (اختياري).
- مهمة العمل والتحدي القصير: 15 دقيقة.
ما تحتاج إليه: بيئتك من L21، واتصال بالإنترنت مرة واحدة من أجل التثبيت، ومجلد practice_files (L52_starter.ipynb؛ وu13_data/orders_year.csv).
2. هدف التعلّم
في نهاية هذا الدرس ستكون قادرًا على أن:
- تلخّص متغيرًا رقميًا بالمتوسط والوسيط والانحراف المعياري والمدى الربيعي والمئينات؛
- تُظهر شكله بمدرج تكراري ومخطط صندوقي؛
- تختار الملخّص المناسب لتوزيع ملتوٍ وتبرّر اختيارك.
الهدف المعتمد من D.World (بنصّه): Summarise a numeric variable with mean, median, standard deviation, IQR and percentiles, show its shape with a histogram and box plot, and explain which summary fits a skewed distribution.
3. لماذا يهمّك هذا؟
يعرف مديرو Cedar Lane رقمًا واحدًا عن الطلبات: متوسط قيمة السلة. لكن بعض الطلبات الكبيرة جدًا تسحب المتوسط إلى أعلى. فإذا كان المتوسط 23.58، فهل الطلب "المعتاد" في حدود 23؟ معظم الطلبات أصغر من ذلك. والمتجر الذي يخطط عروضه حول "العميل المتوسط" يخطط لشخص نادرًا ما يظهر.
يبدأ الإحصاء بوصف البيانات بأمانة: أين يقع المنتصف، وكم تنتشر القيم، وما شكل التوزيع. والملخّص الصحيح يعتمد على الشكل، والشكل شيء تراه في رسم قبل أن تثق بأي رقم.
المبدأ الأساسي: انظر إلى الشكل أولًا، ثم اختر الملخّص الذي يصفه؛ وفي المتغير الملتوي، يصف الوسيط والمئينات الحالة المعتادة عادةً أفضل من المتوسط.
4. المفهوم
الجزء A — التجهيز: SciPy وMatplotlib
تضيف U13 مكتبتين إلى بيئتك من المرحلة 2: SciPy (الدوال الإحصائية) وMatplotlib (الرسوم). وبعد تفعيل بيئتك الافتراضية (كما في L21)، ثبّتهما مرة واحدة:
python -m pip install "scipy==1.18.*" "matplotlib==3.11.*"
ثم افحص الإصدارات في دفتر:
import sys
import matplotlib
import numpy as np
import pandas as pd
import scipy
import matplotlib.pyplot as plt
print("Python ", sys.version.split()[0])
print("NumPy ", np.__version__)
print("pandas ", pd.__version__)
print("SciPy ", scipy.__version__)
print("Matplotlib", matplotlib.__version__)
orders = pd.read_csv("u13_data/orders_year.csv")
print(orders.shape)
Python 3.13.16
NumPy 2.5.3
pandas 3.0.5
SciPy 1.18.1
Matplotlib 3.11.2
(2400, 6)
يجب أن تبدأ إصداراتك بـ 1.18 و3.11 (أُنتجت هذه الصفحة بإصدارات أقدم قليلًا؛ والدوال المستخدمة تعمل بالطريقة نفسها). وإذا فشل التثبيت، فراجع ملاحظات حل المشكلات في L21 وسجّل الخطأ؛ ولا تكمل بخليط من البيئات.
الجزء B — المركز: المتوسط والوسيط
المتوسط (mean) هو المجموع مقسومًا على العدد. والوسيط (median) هو القيمة الوسطى عندما تُرتَّب القيم (نصفها أقل منه، ونصفها أعلى).
bv = orders["basket_value"]
print("mean ", round(bv.mean(), 2))
print("median", round(bv.median(), 2))
print("share of orders below the mean:", round((bv < bv.mean()).mean(), 3))
mean 23.58
median 19.47
share of orders below the mean: 0.624
أكثر من نصف الطلبات كلها أقل من المتوسط. فبعض الطلبات الكبيرة تسحب المتوسط إلى أعلى؛ أما الوسيط فلا يتحرك معها.
الجزء C — الانتشار: التباين والانحراف المعياري والمئينات والمدى الربيعي
الانتشار (spread) يصف مدى بُعد القيم عن المنتصف.
- التباين (variance) هو متوسط مربع المسافة عن المتوسط؛ والانحراف المعياري (standard deviation، SD) هو جذره التربيعي، بوحدات البيانات نفسها.
- في العينة (sample)، يُقسم التباين على n − 1 بدلًا من n ("ddof = 1"، أي delta degrees of freedom فرق درجات الحرية). تستخدم pandas افتراضيًا
ddof=1؛ أما NumPy فتستخدم افتراضيًاddof=0. فاذكر دائمًا أيهما استخدمت. - المئين (percentile) هو القيمة التي تقع تحتها حصة معيّنة من البيانات: فالمئين التسعون تقع تحته 90% من القيم. والربيعات (quartiles) هي المئينات 25 و50 (الوسيط) و75. والمدى الربيعي (IQR، interquartile range) هو Q3 − Q1: مدى النصف الأوسط.
print("SD pandas (ddof=1):", round(bv.std(), 4))
print("SD NumPy (ddof=0):", round(np.std(bv.to_numpy()), 4))
print("SD NumPy (ddof=1):", round(np.std(bv.to_numpy(), ddof=1), 4))
q1, q3 = bv.quantile([0.25, 0.75])
print("Q1", round(q1, 2), "| Q3", round(q3, 2), "| IQR", round(q3 - q1, 2))
print("P10, P90:", bv.quantile([0.10, 0.90]).round(2).tolist())
SD pandas (ddof=1): 16.8777
SD NumPy (ddof=0): 16.8742
SD NumPy (ddof=1): 16.8777
Q1 12.92 | Q3 29.02 | IQR 16.1
P10, P90: [8.94, 42.43]
تختلف قيمتا الانحراف المعياري قليلًا بسبب n مقابل n − 1. ومع 2,400 قيمة يكون الفرق ضئيلًا؛ أما مع 10 قيم فلا.
الجزء D — الشكل: الالتواء والقيم المتطرفة
يكون التوزيع ملتويًا (skewed) عندما يكون أحد ذيليه أطول من الآخر. وقيم الطلبات ملتوية إلى اليمين (right-skewed): طلبات صغيرة ومتوسطة كثيرة، وذيل طويل من الطلبات الكبيرة. ومن علامات الالتواء إلى اليمين: المتوسط > الوسيط، والمسافة من الوسيط إلى المئين التسعين أكبر من المسافة من المئين العاشر إلى الوسيط.
from scipy import stats
print("median - P10:", round(bv.median() - bv.quantile(0.10), 2))
print("P90 - median:", round(bv.quantile(0.90) - bv.median(), 2))
print("skewness (SciPy):", round(stats.skew(bv), 2))
median - P10: 10.53
P90 - median: 22.96
skewness (SciPy): 3.4
معامل الالتواء القريب من 0 يعني توزيعًا متماثلًا تقريبًا؛ والموجب يعني ذيلًا أيمن أطول. وفي التقارير، صِف الالتواء بالكلمات ("معظم الطلبات بين 13 و29؛ وقليل منها يتجاوز 100").
القيم المتطرفة تحرّك المتوسط والانحراف المعياري كثيرًا، والوسيط والمدى الربيعي قليلًا جدًا. أضف طلبًا متطرفًا واحدًا وقارن:
with_extreme = pd.concat([bv, pd.Series([2500.0])], ignore_index=True)
for name, s in [("original", bv), ("plus one 2,500 order", with_extreme)]:
print(f"{name:22} mean {s.mean():7.2f} median {s.median():6.2f} SD {s.std():7.2f} IQR {s.quantile(0.75) - s.quantile(0.25):6.2f}")
original mean 23.58 median 19.47 SD 16.88 IQR 16.10
plus one 2,500 order mean 24.61 median 19.47 SD 53.28 IQR 16.12
طلب واحد من 2,401 غيّر المتوسط بنحو يورو واحد وضاعف الانحراف المعياري أكثر من ثلاث مرات، بينما لم يتحرك الوسيط والمدى الربيعي تقريبًا. وفي هذا الدرس أنت تصف القيم المتطرفة؛ أما هل تُبقيها أو تقيّدها أو تحقق فيها فهو قرار بيانات تدرّبت عليه في U09 (L38).
الجزء E — الرسوم: المدرج التكراري والمخطط الصندوقي باستخدام fig, ax
الأمر fig, ax = plt.subplots() في Matplotlib ينشئ شكلًا (figure) (الصورة كلها) ومحاور (axes) (منطقة رسم واحدة). وأنت ترسم على ax. وهذه هي الواجهة التي تبني عليها U15.
fig, ax = plt.subplots(figsize=(7, 3.5))
counts, edges, _ = ax.hist(bv, bins=40)
ax.set_xlabel("Basket value (€)")
ax.set_ylabel("Number of orders")
ax.set_title("Basket value per order, synthetic year")
print("bins:", len(counts), "| counts add up to rows:", int(counts.sum()) == len(bv))
print("tallest bin from", round(edges[counts.argmax()], 1), "to", round(edges[counts.argmax() + 1], 1))
bins: 40 | counts add up to rows: True
tallest bin from 14.0 to 20.2
المدرج التكراري (histogram) يقسم المدى إلى فئات (bins) ويعدّ القيم في كل منها. واختيار الفئات يغيّر الصورة: الفئات القليلة جدًا تُخفي الشكل، والكثيرة جدًا تجعله مشوَّشًا. جرّب 10 و40 و100 فئة، واحتفظ بالتي تُظهر الشكل بأمانة. ويجب أن يساوي مجموع الأعداد عدد الصفوف.
المخطط الصندوقي (box plot) يُظهر الوسيط (خط)، والربيعات (الصندوق)، و"الشعيرات" (whiskers) حتى 1.5 × المدى الربيعي خارج الصندوق؛ والنقاط الأبعد تُرسم منفردة:
fig, ax = plt.subplots(figsize=(7, 3))
bp = ax.boxplot([orders.loc[orders["region_code"] == r, "basket_value"] for r in ["CEN", "HAR", "NOR", "STA", "WEB"]],
tick_labels=["CEN", "HAR", "NOR", "STA", "WEB"], orientation="horizontal")
ax.set_xlabel("Basket value (€)")
print("points drawn beyond the whiskers:", sum(len(f.get_xdata()) for f in bp["fliers"]))
points drawn beyond the whiskers: 107
المخططات الصندوقية جيدة لمقارنة المجموعات جنبًا إلى جنب. والنقاط المنفردة قيم بعيدة عن المنتصف، لا "أخطاء".
الجزء F — إعادة قراءة describe()
الدالة describe() (L25) تطبع معظم هذه الملخّصات دفعة واحدة. والآن تستطيع قراءة كل سطر:
print(bv.describe().round(2).to_string())
count 2400.00
mean 23.58
std 16.88
min 1.47
25% 12.92
50% 19.47
75% 29.02
max 251.25
std يستخدم ddof = 1؛ و25% و50% و75% هي الربيعات؛ وmin وmax يُظهران الطرفين. قارن mean و50% أولًا: إذا اختلفا كثيرًا، فالتوزيع ملتوٍ.
5. مثال محلول
Cedar Lane (نشاط خيالي)، بيانات مصطنعة. يسأل مدير الخدمات اللوجستية: "How many days does a delivery usually take? I've been quoting customers the average." (أي: "كم يومًا يستغرق التوصيل عادةً؟ كنت أذكر للعملاء المتوسط.")
الخطوة 1 — انظر إلى الشكل. العمود delivery_days عدد صحيح من الأيام.
dd = orders["delivery_days"]
print(dd.value_counts().sort_index().to_string())
delivery_days
1 472
2 976
3 581
4 239
5 79
6 32
7 12
8 6
9 1
10 2
معظم عمليات التوصيل تستغرق 1–3 أيام؛ وذيل رفيع يمتد إلى 10. ملتوٍ إلى اليمين.
الخطوة 2 — الملخّصات.
print("mean", round(dd.mean(), 2), "| median", dd.median(), "| P90", dd.quantile(0.90), "| P95", dd.quantile(0.95))
print("share delivered within 3 days:", round((dd <= 3).mean(), 3))
mean 2.45 | median 2.0 | P90 4.0 | P95 5.0
share delivered within 3 days: 0.845
الخطوة 3 — اختر الملخّص المناسب للغرض. في وعد يُقدَّم للعميل، لا تكفي قيمة معتادة: المهم كم مرة يُوفى بالوعد. فعبارة "خلال 3 أيام" تغطي حوالي 85% من الطلبات؛ و"خلال 4 أيام" تغطي أكثر. أما المتوسط (2.45 يوم) فليس وعدًا يستطيع أحد الوفاء به.
الخطوة 4 — ارسم.
fig, ax = plt.subplots(figsize=(6, 3))
counts, edges, _ = ax.hist(dd, bins=range(1, 12), align="left", rwidth=0.9)
ax.set_xlabel("Delivery days"); ax.set_ylabel("Orders"); ax.set_xticks(range(1, 11))
print("histogram counts:", [int(c) for c in counts], "| total:", int(counts.sum()))
histogram counts: [472, 976, 581, 239, 79, 32, 12, 6, 1, 2] | total: 2400
مع قيم بالأيام الكاملة، تُظهر فئة لكل يوم (bins=range(1, 12)) الشكل بدقة.
الخطوة 5 — الوصف للمدير: "نصف عمليات التوصيل تصل خلال يومين، وحوالي 85% خلال 3 أيام؛ وحوالي واحدة من كل 20 تستغرق 5 أيام أو أكثر. اذكر '2–3 أيام' مع 'حتى 5 أيام' كحد أعلى صادق، بدلًا من المتوسط البالغ 2.45 يوم."
6. شرح بصري
RIGHT-SKEWED (basket value) BOX PLOT
count ├──────[ Q1 │median│ Q3 ]──────┤ · · ·
██ whisker IQR box whisker points beyond
███ (middle half) 1.5 × IQR
█████▄
███████▄▂ long right tail MEAN is pulled toward the tail; MEDIAN stays in the middle
█████████▆▄▃▂▁▁ ▁ ▁ · ADD ONE EXTREME VALUE → mean, SD jump; median, IQR barely move
──────────────────────────────────▶ value
↑median ↑mean SD: pandas ddof=1, NumPy ddof=0 by default — say which
وصف الرسم: على اليسار مدرج تكراري لمتغير ملتوٍ إلى اليمين: أعمدة طويلة عند القيم المنخفضة وذيل طويل رفيع إلى اليمين، مع تعليم الوسيط إلى يسار المتوسط. وعلى اليمين مخطط صندوقي أفقي: شعيرات، وصندوق من الربيع الأول إلى الربيع الثالث والوسيط داخله، ونقاط منفردة أبعد من 1.5 ضعف المدى الربيعي. وملاحظات: المتوسط يُسحب نحو الذيل بينما يبقى الوسيط في المنتصف؛ وإضافة قيمة متطرفة واحدة تجعل المتوسط والانحراف المعياري يقفزان بينما لا يتحرك الوسيط والمدى الربيعي تقريبًا؛ وتستخدم pandas افتراضيًا ddof يساوي واحدًا وNumPy ddof يساوي صفرًا. (نصوص الرسم: RIGHT-SKEWED = ملتوٍ إلى اليمين؛ BOX PLOT = المخطط الصندوقي؛ whisker = شعيرة؛ IQR box (middle half) = صندوق المدى الربيعي (النصف الأوسط)؛ points beyond 1.5 × IQR = نقاط أبعد من 1.5 × المدى الربيعي؛ long right tail = ذيل أيمن طويل؛ say which = اذكر أيهما.)
7. تدريب موجَّه
افتح L52_starter.ipynb.
- شغّل خلية التجهيز وسجّل إصداراتك. هل إصدارا SciPy وMatplotlib هما المتوقعان؟
- قيم السلال: المتوسط، والوسيط، والانحراف المعياري (ddof = 1)، وQ1 وQ3 والمدى الربيعي وP90. ارسم مدرجًا تكراريًا (جرّب 10 و40 و100 فئة؛ واحتفظ بواحدة) وتحقّق من أن مجموع أعداده يساوي الصفوف.
- أيام التوصيل: الملخّصات نفسها، ومدرج تكراري بفئة لكل يوم.
- أضف قيمة سلة متطرفة واحدة من اختيارك (مثل 3,000) إلى نسخة من قيم السلال. وقارن كل الملخّصات قبل وبعد في جدول صغير واحد.
- أعد حساب الانحراف المعياري والمدى الربيعي لقيم السلال باستخدام NumPy (
np.std(..., ddof=1)وnp.percentile) وطابِقهما مع pandas. - شغّل Restart Kernel and Run All Cells….
8. تدريب مستقل
وصف التوزيع لكل منطقة. لا يعرف مديرو المناطق إلا متوسط قيمة السلة عندهم.
- لكل منطقة، احسب الملخّصات التي تراها لازمة للمدير، وارسم رسمًا واحدًا للمقارنة.
- قرّر أي ملخّص يصف الطلب "المعتاد" في كل منطقة، وبرّر اختيارك من الشكل.
- اكتب وصفًا من 120 إلى 180 كلمة للمديرين: كيف يبدو الطلب المعتاد، وكم تنتشر الطلبات، وما الذي يُخفيه المتوسط. واستخدم مئينًا واحدًا على الأقل.
- تحقّق من أرقامك بطريقة ثانية (NumPy، أو حساب يدوي لمئين) لمنطقة واحدة.
اعمل في L52_starter.ipynb (الجزء 2).
9. اختبار الفهم (Knowledge Check) — وصف البيانات
Q1. متغير متوسطه 23.6 ووسيطه 19.5. ماذا يخبرك هذا على الأرجح؟
- A) أنه متماثل
- B) أن البيانات فيها أخطاء
- C) أنه ملتوٍ إلى اليمين: ذيل من القيم الكبيرة يسحب المتوسط إلى أعلى
- D) أنه ملتوٍ إلى اليسار
Q2. أي ملخّص يتغير أقل من غيره عندما تضيف قيمة واحدة كبيرة جدًا؟
- A) الحد الأقصى
- B) الانحراف المعياري
- C) المتوسط
- D) الوسيط
Q3. الدالة s.std() في pandas والدالة np.std(a) في NumPy تعطيان رقمين مختلفين قليلًا على البيانات نفسها. لماذا؟
- A) pandas تستخدم ddof = 1 (تقسم على n − 1)، وNumPy تستخدم افتراضيًا ddof = 0
- B) تستخدمان وحدات مختلفة
- C) NumPy أقل دقة
- D) pandas تتجاهل أكبر قيمة
Q4. ما المدى الربيعي (IQR)؟
- A) المئين 75 ناقص المئين 25
- B) المتوسط ناقص الوسيط
- C) الحد الأقصى ناقص الحد الأدنى
- D) انحرافان معياريان
Q5. ماذا يعني المئين التسعون لأيام التوصيل؟
- A) 90 يومًا
- B) 90% من عمليات التوصيل تستغرق هذا العدد من الأيام أو أقل
- C) 90% من عمليات التوصيل تستغرق هذا العدد من الأيام بالضبط
- D) متوسط أبطأ 10%
Q6. مجموع أعداد مدرج تكراري أقل من عدد الصفوف. بماذا يجب أن تشك؟
- A) بعض القيم وقعت خارج مدى الفئات (أو كانت مفقودة) ولم تُعدّ
- B) المتغير ملتوٍ
- C) الفئات عريضة جدًا
- D) لا شيء، فالمدرجات التكرارية تقرّب الأعداد
Q7. في المخطط الصندوقي، ما النقاط المنفردة الواقعة خارج الشعيرات؟
- A) المتوسط
- B) أخطاء يجب حذفها
- C) قيم مفقودة
- D) قيم أبعد من 1.5 × المدى الربيعي خارج الصندوق؛ تستحق نظرة، وليست أخطاء تلقائيًا
Q8. يريد مدير أن يَعِد العملاء بوقت توصيل يُوفى به 9 مرات من كل 10. أي ملخّص يساعد أكثر؟
- A) المتوسط
- B) الانحراف المعياري
- C) الحد الأدنى
- D) المئين التسعون
10. الذكاء الاصطناعي في التطبيق (اختياري، وفي حدود)
الهدف: أن تسأل مساعد ذكاء اصطناعي أي ملخّص يناسب متغيرًا ملتويًا إلى اليمين، ثم تتحقق من ادعائه مقابل رسمك وأرقامك.
- صِف متغير أيام التوصيل بالكلمات (أيام كاملة، من 1 إلى 10، ومعظمها 1–3) واسأل:
"Which summary statistics should I report for a right-skewed delivery-time variable, and why?"
وبالعربية: "ما الإحصاءات الملخِّصة التي يجب أن أذكرها لمتغير وقت توصيل ملتوٍ إلى اليمين، ولماذا؟"
- قارن إجابته بملخّصاتك ومدرجك التكراري.
الحدود:
- استخدام الذكاء الاصطناعي اختياري.
- شارك وصفًا وأرقامًا ملخِّصة، لا بيانات عملاء خامًا.
- الذكاء الاصطناعي لا يرى رسمك؛ والتوصية التي تتجاهل الشكل الذي تراه خاطئة بالنسبة إلى بياناتك.
11. التحقق من مخرجات الذكاء الاصطناعي
- الشكل: هل تطابق نصيحته الشكل الذي في مدرجك التكراري؟
- الأرقام: احسب كل ملخّص يوصي به. هل يقول شيئًا مفيدًا للمدير؟
- ddof: إذا أعطى صيغة للانحراف المعياري، فهل يذكر n أم n − 1؟
- دوّن توصية واحدة تقبلها، وواحدة ترفضها، مع السبب.
12. منظور الأعمال
المتوسطات تُخفي العميل الذي تخطط من أجله. فحدّ العروض الترويجية، أو وعد التوصيل، أو خطة التوظيف المبنية على متوسط متغير ملتوٍ، مبنية لعميل نادرًا ما يظهر. أما الوسيط والمئينات فتصف الحالة المعتادة والذيل كلًّا على حدة، وهذا بالضبط ما تحتاجه القرارات التشغيلية ("معظم الطلبات"، "9 من كل 10 عمليات توصيل").
وعرض الشكل (مدرج تكراري أو مخطط صندوقي واحد) بجانب الأرقام هو أسرع طريقة لإيقاف اجتماع عن الجدال حول متوسط.
13. مهمة عمل — حدّ التوصيل المجاني
نشاط خيالي: Cedar Lane. يقترح التسويق توصيلًا مجانيًا للطلبات "الأعلى من متوسط قيمة السلة".
- احسب حصة الطلبات التي ستستحق التوصيل المجاني عند المتوسط، وعند الوسيط، وعند المئين 75.
- اكتب مذكرة قصيرة (من 80 إلى 120 كلمة) تشرح لماذا لا تعني عبارة "أعلى من المتوسط" هنا "أعلى من معظم الطلبات"، وأي حدّ ستناقشه بدلًا من ذلك، بحسب الهدف (مكافأة أكبر ربع من الطلبات، أو نصف الطلبات كلها).
14. تحدٍّ قصير — الانحراف المعياري الذي كان أصغر من اللازم
نشاط خيالي: Cedar Lane. أبلغ زميل عن الانحراف المعياري لأكبر عشر قيم سلال باستخدام NumPy، وقارنه بقيمتك في pandas.
top10 = bv.nlargest(10)
print(round(np.std(top10.to_numpy()), 3), round(top10.std(), 3))
- قبل تشغيله، توقّع أي الرقمين أكبر ولماذا.
- شغّله واشرح الفرق عندما n = 10 مقارنةً بـ n = 2,400.
- اكتب استدعاء NumPy الذي يطابق pandas.
- اكتب جملة واحدة يحتفظ بها زميلك كقاعدة.
سلّم عملك قبل أن تتلقى الملاحظات. لا يوجد حل منشور هنا.
15. المصادر
مطلوب (حوالي 40 دقيقة):
- Think Stats, 3rd edition (Allen B. Downey، دار Green Tea Press؛ مجاني على الإنترنت مع دفاتر)، الفصول الخاصة بالتوزيعات والإحصاءات الملخِّصة. https://greenteapress.com/wp/think-stats-3e/ (فُتح في 20 أكتوبر 2026.)
- Matplotlib: Quick start guide، القسمان الخاصان بالأشكال والمحاور (
fig, ax = plt.subplots()). https://matplotlib.org/stable/users/explain/quick_start.html
موصى به:
- OpenIntro: Introduction to Modern Statistics (2nd ed.)، كتاب مجاني بصيغة PDF وعلى الويب: الفصل الخاص باستكشاف البيانات الرقمية. https://www.openintro.org/book/ims/
- Practical Statistics for Data Scientists, 2nd ed. (Bruce وBruce وGedeck؛ O'Reilly، 2020)، الفصل 1 (كتاب مدفوع؛ والنصوص المجانية أعلاه تغطي المحتوى المطلوب). https://www.oreilly.com/library/view/practical-statistics-for/9781492072935/
مرجع (صفحات التثبيت):
- PyPI: scipy — https://pypi.org/project/scipy/ · PyPI: matplotlib — https://pypi.org/project/matplotlib/
متابعة مفتوحة (D.World، SN06): لم يُختبر بعد تثبيت SciPy 1.18.x وMatplotlib 3.11.x على Python 3.14 وعلى Windows/macOS لهذه الدورة.
16. شخصية تتابعها / قدوة
Allen B. Downey (اختارته D.World للدرسين L52–L53).
- حقيقة مُتحقَّق منها: مؤلف Think Stats (الطبعة الثالثة)، المنشور مجانًا على الإنترنت مع دفاتر قابلة للتشغيل لدى Green Tea Press (خريطة D.World للمرحلة 3).
- لماذا توصي به D.World: كتبه تعلّم الإحصاء من خلال الكود والمحاكاة، وهو النهج نفسه الذي تستخدمه U13.
- ملاحظة صادقة: لا نذكر إلا الحقيقة المذكورة أعلاه.
- جرّب هذا: افتح صفحة Think Stats وابحث عن الفصل الذي يقدّم المئينات. كيف يعرّف رتبة المئين (percentile rank)؟
17. المصطلحات
| المصطلح | معناه في هذا الدرس |
|---|---|
| المتوسط (Mean) | المجموع مقسومًا على العدد؛ وتسحبه القيم المتطرفة |
| الوسيط (Median) | القيمة الوسطى في البيانات المرتّبة؛ نصفها أقل منه ونصفها أعلى |
| الانحراف المعياري (Standard deviation) | المسافة المعتادة عن المتوسط، بوحدات البيانات (والانحراف المعياري للعينة يستخدم n − 1) |
| التباين (Variance) | مربع الانحراف المعياري |
| المئين (Percentile) | القيمة التي تقع تحتها حصة معيّنة من البيانات |
| المدى الربيعي (IQR) | المئين 75 ناقص المئين 25؛ مدى النصف الأوسط |
| الالتواء (Skew) | عدم تماثل التوزيع؛ والالتواء إلى اليمين = ذيل طويل من القيم الكبيرة |
| المدرج التكراري (Histogram) | رسم لأعداد القيم في كل فئة من متغير رقمي |
| المخطط الصندوقي (Box plot) | رسم للوسيط والربيعات والشعيرات والنقاط البعيدة عن المنتصف |
للمراجعة: التجميع (L23) · describe() (L25) · القيم المتطرفة (L38).
18. تأمّل
- أي ملخّص ستذكره أولًا الآن لمتغير ملتوٍ، ولماذا؟
- متى يكون للفرق بين ddof = 0 وddof = 1 أهمية؟
- أين يُخفي متوسطٌ ذيلًا طويلًا في عملك أنت؟
19. الخطوة التالية
أصبحت الآن قادرًا على وصف متغير بأمانة. والتالي:
- L53 — الاحتمالات والتوزيعات لعمل البيانات (Probability & Distributions for Data Work). من وصف ما حدث إلى نمذجة الصدفة: الاحتمالات الشرطية والمحاكاة.
20. أدلة التقييم
| الدليل | ما الذي يُظهره | كيف يُقيَّم |
|---|---|---|
| فحص البيئة + وصف التوزيع لكل منطقة (التدريب المستقل) | تسجيل الإصدارات؛ وملخّصات مختارة من الشكل؛ ورسم مقارنة؛ ووصف من 120 إلى 180 كلمة فيه مئين؛ وتحقّق بطريقة ثانية | دليل التقييم L52-IP |
| التدريب الموجَّه (السلال، وأيام التوصيل، والقيمة المتطرفة) | ملخّصات صحيحة، وddof مطابَق، وأعداد المدرج التكراري مفحوصة، وأثر القيمة المتطرفة معروض | دليل التقييم L52-GP |
| اختبار الفهم (Q1–Q8) | المركز، والانتشار، وddof، والمئينات، والشكل، والمخططات الصندوقية | 7 إجابات صحيحة أو أكثر من 8 |
| مهمة العمل والتحدي القصير | التفكير في الحدود بالحصص؛ وشرح ddof عندما تكون n صغيرة | دليلا التقييم L52-WT وL52-MC |
المراجع
- Downey, A. B. Think Stats, 3rd ed. Green Tea Press. https://greenteapress.com/wp/think-stats-3e/ (TS3). يدعم: التوزيعات والإحصاءات الملخِّصة؛ وحقيقة القدوة.
- Matplotlib developers. Quick start guide. https://matplotlib.org/stable/users/explain/quick_start.html (MPL-QS). يدعم:
fig, ax = plt.subplots()وhist وboxplot. - OpenIntro. Introduction to Modern Statistics, 2nd ed. https://www.openintro.org/book/ims/ (OI-IMS). يدعم: الملخّصات الرقمية.
- Bruce, P., Bruce, A., Gedeck, P. Practical Statistics for Data Scientists, 2nd ed. O'Reilly, 2020. https://www.oreilly.com/library/view/practical-statistics-for/9781492072935/ (PSDS).
- PyPI: scipy; matplotlib. https://pypi.org/project/scipy/ · https://pypi.org/project/matplotlib/ (PYPI-SC, PYPI-MPL). يدعم: إصدارات التثبيت.
- D.World Stage 3 Detailed Lesson Map v0.9 (صف L52؛ SN06؛ SN04). وثيقة داخلية.
L53 · Probability & Distributions for Data Work Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
L54 · Sampling, Standard Error & Confidence Intervals Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
L55 · Hypothesis Tests, Experiments & Significance Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
L56 · Correlation, Association & Causation Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
U13 Exit = Project 6 — Experiment / Statistical Investigation Membersللمشتركين
The exit brief is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
تكليف نهاية الوحدة ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
U14 · Exploratory Data Analysisالتحليل الاستكشافي للبيانات
Stage 3المرحلة 3Free: the first lesson. The other lessons, the exit brief and the practice files are for Starter and Membership.
مجاني: الدرس الأول. باقي الدروس وتكليف نهاية الوحدة وملفات التمرين لأصحاب باقة البداية والاشتراك.
Download U14 practice files (.zip, 292 KB)حمّل ملفات تمرين U14 (zip، 292 KB)
Unzip into a short folder such as D:\DWorld on Windows. The zip has only learner files: no answers.
فك الضغط في مجلد قصير مثل D:\DWorld على ويندوز. الملف يحتوي ملفات المتعلم فقط بدون إجابات.
L57 · Structured EDA: From Business Question to Analysis PlanL57 · التحليل الاستكشافي المنظَّم: من سؤال العمل إلى خطة التحليل Freeمجاني
L57 — Structured EDA: From Business Question to Analysis Plan
Learner file. Answers and model solutions are in a separate instructor-only file. Version: U14 Production v2 · Content date: 20 October 2026. Time estimates are planning estimates until learners have tested them. The code in this lesson was run with the course setup from L21 (Python 3.13, pandas 3.0 and NumPy 2.5; SciPy 1.18 and Matplotlib 3.11 where used) on Linux and on Windows 11 on 4 November 2026 (see the Code Test Report).
All businesses are fictional and all data is synthetic. Figures are drawn in your notebook; this page shows the numbers printed by the same code.
1. Lesson Identity
| Field | Value |
|---|---|
| Title | Structured EDA: From Business Question to Analysis Plan |
| Stage | Stage 3 — Data Analysis |
| Unit | U14 — Exploratory Data Analysis |
| Lesson ID | L57 |
| Skills | EDA purpose; question trees; testable hypotheses; data-readiness checks; univariate profiling; the EDA log; stopping rules |
| Difficulty | Intermediate |
| Estimated time | About 2 hours (planning estimate, unvalidated) |
| Assessment level | Practice / Application |
| Prerequisites | U09 (validation checks), L27 (grouped KPIs), L52 (describing distributions), L56 (association is not cause). |
Time breakdown (planning estimate):
- Concept and worked example: 45 min.
- Guided practice: 25 min.
- Independent practice: 25 min.
- Knowledge check: 10 min.
- AI in Practice and Verification: 5 min (optional).
- Workplace task and mini challenge: 10 min.
What you need: your U13 environment and practice_files (L57_starter.ipynb; in u14_data: orders_2026.csv, customers_2026.csv).
The U14 data. orders_2026.csv has one row per Cedar Lane order from January to September 2026 (order date, region, pickup point, channel, main category, basket value, distance, delivery hours, a late flag for deliveries over 48 hours, and a refund flag). customers_2026.csv has one row per customer (region, pickup point, sign-up channel, app user, distance, first-order details, whether they ordered again within 90 days of their first order, and two summary columns for the whole period). All five U14 lessons use these two files.
2. Learning Objective
By the end of this lesson, you will be able to:
- turn a vague business concern into an EDA plan: a question tree, testable hypotheses and the data checks you need;
- run the data-readiness checks and a first univariate pass;
- keep an EDA log that links every finding to the code and the number behind it.
D.World approved objective (verbatim): Turn a vague business concern into an EDA plan with questions, hypotheses, required data checks and an EDA log, and complete the first univariate pass.
3. Why It Matters
Concerns rarely arrive as questions. They arrive as feelings: "online orders feel slower this month", "Harbor customers don't come back like they used to". The easiest response is to open the data and start drawing charts. Two hours later there are thirty charts, no answer, and a manager who still feels the same.
Exploratory data analysis (EDA) is the investigation you do before deciding or modelling: what does the data say about this concern, and where should we look next? Done well, it is structured. You write down what you want to know, check the data is fit to answer, look at one thing at a time, and record what you found and how sure you are.
Key principle: EDA starts from a written question and ends with logged, evidence-backed findings; every chart must serve a question in the plan.
4. Concept
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
orders = pd.read_csv("u14_data/orders_2026.csv", parse_dates=["order_date"])
customers = pd.read_csv("u14_data/customers_2026.csv")
print(orders.shape, customers.shape)
print(orders.dtypes.astype(str).to_dict())
(4812, 13) (2198, 12)
{'order_id': 'str', 'customer_id': 'str', 'order_date': 'datetime64[us]', 'weekday': 'str', 'region': 'str', 'pickup_point': 'str', 'channel': 'str', 'main_category': 'str', 'basket_value': 'float64', 'distance_km': 'float64', 'delivery_hours': 'float64', 'late': 'int64', 'refunded': 'int64'}
Part A — What EDA is for
EDA investigates data before you decide, test or model. Its outputs are not charts; they are findings ("September online deliveries took about 9 hours longer than earlier months, n = …") and hypotheses worth checking. It is different from:
- data cleaning (U09): EDA only checks that the data are fit to explore; it does not repair them;
- hypothesis testing (L55): EDA suggests hypotheses; a test or an experiment can confirm one later;
- presentation (U15): EDA charts are working views; polished charts come later.
Part B — The question tree
A question tree breaks a vague concern into specific questions that data can answer. Start with the concern at the top; each branch asks "what would have to be true?":
"Online orders feel slower this month"
├── Is it real? Q1 Are online delivery hours higher in September than before? (how much, for how many orders)
├── Where? Q2 Is it all regions / pickup points, or some?
│ Q3 Is it web and app, or one channel?
├── Is it the mix? Q4 Did September have more long-distance or Monday orders (which are slower anyway)?
└── Is it the data? Q5 Are September's dates and hours complete and in the usual range?
Each leaf is answerable with one table or plot. Branches like "Is it the mix?" and "Is it the data?" protect you from the most common wrong answers.
Part C — Hypotheses as testable statements
A hypothesis in EDA is a specific statement that data could contradict:
- ✘ "Something is wrong with online delivery." (Not testable.)
- ✔ "September online orders took longer to deliver than January–August online orders, in every region." (Testable: compare by month and region.)
- ✔ "The September increase is explained by more long-distance orders." (Testable: compare delivery hours within distance bands.)
Write hypotheses before exploring, and add new ones as you go, marking when each was added.
Part D — Data-readiness checks
Before exploring, confirm the data are fit for the question. These are the U09 checks, run quickly and logged, not a cleaning project:
checks = {
"rows": len(orders),
"duplicate order_id": int(orders["order_id"].duplicated().sum()),
"missing values (any column)": int(orders.isna().sum().sum()),
"date range": f"{orders['order_date'].min().date()} to {orders['order_date'].max().date()}",
"days with no orders": [d.date().isoformat() for d in pd.date_range(orders["order_date"].min(), orders["order_date"].max()).difference(orders["order_date"])],
"basket_value <= 0": int((orders["basket_value"] <= 0).sum()),
"delivery_hours range": f"{orders['delivery_hours'].min()} to {orders['delivery_hours'].max()}",
"late flag consistent with hours > 48": bool(((orders["delivery_hours"] > 48).astype(int) == orders["late"]).all()),
}
for k, v in checks.items():
print(f"{k:40} {v}")
rows 4812
duplicate order_id 0
missing values (any column) 0
date range 2026-01-01 to 2026-09-30
days with no orders ['2026-01-06']
basket_value <= 0 0
delivery_hours range 4.0 to 69.6
late flag consistent with hours > 48 True
No duplicates, no missing values, values in range, and the late flag agrees with the hours. One calendar day has no orders at all. That is a finding to log and check (a closure? a missing data load?) before it can distort a daily chart; for a monthly delivery-time question it does not change the picture. If a check had shown a large gap, the first finding would be "data problem", and the investigation would change.
Part E — The first univariate pass
Univariate analysis looks at one variable at a time (L52): its distribution, centre, spread and anything odd. Profile every variable that appears in the plan, not every column in the file:
def profile(s):
"""Short profile of a numeric Series: n, missing, centre, spread, tails."""
return {"n": int(s.count()), "missing": int(s.isna().sum()), "mean": round(float(s.mean()), 2), "median": round(float(s.median()), 2),
"p10": round(float(s.quantile(0.10)), 2), "p90": round(float(s.quantile(0.90)), 2), "max": round(float(s.max()), 2)}
for col in ["delivery_hours", "distance_km", "basket_value"]:
print(col, profile(orders[col]))
print(orders["channel"].value_counts().to_dict())
delivery_hours {'n': 4812, 'missing': 0, 'mean': 33.1, 'median': 32.8, 'p10': 20.4, 'p90': 45.79, 'max': 69.6}
distance_km {'n': 4812, 'missing': 0, 'mean': 5.99, 'median': 6.0, 'p10': 1.8, 'p90': 10.0, 'max': 14.0}
basket_value {'n': 4812, 'missing': 0, 'mean': 22.8, 'median': 20.15, 'p10': 10.39, 'p90': 38.09, 'max': 101.1}
{'web': 2097, 'store': 1575, 'app': 1140}
For categories, counts (and shares) are the univariate profile. Note what you see in the log, then move on: the comparison across months and regions is the next step (L58).
Part F — The EDA log
An EDA log is a running table: one row per step, with the question, the method, the finding (with a number and its n), your confidence and the next step. In a notebook, a list of dictionaries turned into a DataFrame works well:
log = []
def log_step(question, method, finding, confidence, next_step):
log.append({"step": len(log) + 1, "question": question, "method": method, "finding": finding,
"confidence": confidence, "next_step": next_step})
log_step("Q5 data fit?", "readiness checks (Part D)", "no duplicates/missing; 1 day with no orders (logged, to check); flags consistent",
"high", "profile key variables")
log_step("profile", "profile() on delivery_hours", f"median {orders['delivery_hours'].median():.1f} h, p90 {orders['delivery_hours'].quantile(0.9):.1f} h, n={len(orders)}",
"high", "compare by month and channel (Q1, Q3)")
print(pd.DataFrame(log).to_string(index=False))
step question method finding confidence next_step
1 Q5 data fit? readiness checks (Part D) no duplicates/missing; 1 day with no orders (logged, to check); flags consistent high profile key variables
2 profile profile() on delivery_hours median 32.8 h, p90 45.8 h, n=4812 high compare by month and channel (Q1, Q3)
Every finding carries a number and the cell that produced it. Confidence is a judgement: "high" for a direct count, "medium" for a pattern in small groups, "low" for a hunch.
Part G — Stopping rules
EDA can go on forever. Decide in the plan when you will stop:
- every question in the tree has an answer (or is marked "cannot answer with this data");
- the top hypotheses are ranked with evidence for and against;
- a fixed time box is reached (for example two hours), and what is left is written as next steps.
Random charting is the opposite of a stopping rule: if a chart does not answer a question in the tree, do not make it (or add the question first).
5. Worked Example
Cedar Lane (fictional), synthetic data. The Station manager says: "Customers keep telling us Station deliveries take too long."
Step 1 — Question tree.
"Station deliveries take too long"
├── Q1 Are Station delivery hours higher than other pickup points? (median, p90, share late)
├── Q2 Is it all of 2026 or recent? (L58/L60 — next step)
└── Q3 Is the data fit? (Station rows present every month; hours in range)
Step 2 — Hypothesis (written before exploring): "Station deliveries are slower than the average pickup point by more than 2 hours at the median."
Step 3 — Data checks for this question.
sta = orders[orders["pickup_point"] == "PK-STA"]
print("Station orders:", len(sta), "| months present:", sta["order_date"].dt.month.nunique(), "of 9")
print("Station hours range:", sta["delivery_hours"].min(), "to", sta["delivery_hours"].max())
Station orders: 644 | months present: 9 of 9
Station hours range: 9.8 to 55.4
Step 4 — Univariate comparison.
summary = orders.groupby("pickup_point")["delivery_hours"].agg(n="count", median="median", p90=lambda s: s.quantile(0.9))
summary["late_share"] = orders.groupby("pickup_point")["late"].mean()
print(summary.round(2).to_string())
n median p90 late_share
pickup_point
PK-CEN 935 24.90 34.90 0.00
PK-HAR 968 30.90 42.10 0.03
PK-NOR 978 37.10 46.86 0.08
PK-STA 644 28.05 37.80 0.01
WEB-HOME 1287 39.50 50.20 0.15
Step 5 — Log and conclusion. Station's median delivery time is not the highest: the home-delivery point (WEB-HOME) and North are slower, because their customers live farther away (distance is a likely explanation to check in L58). The hypothesis is not supported for Station.
Log entry: Q1 · groupby median/p90/late share by pickup point · Station median about 28 h vs 37–40 h for NOR and WEB-HOME; Station late share about 1% (n = 644) · confidence high · next: ask what customers mean by "too long" (expectation vs actual?), compare with promised times.
The finding changes the conversation: the complaint may be about expectations or a few very late orders, not typical delivery time.
6. Visual Explanation
VAGUE CONCERN ──▶ QUESTION TREE ──▶ HYPOTHESES (testable) ──▶ DATA CHECKS ──▶ UNIVARIATE PASS ──▶ (L58–L61) ──▶ RANKED FINDINGS
"feels slower" Is it real? Where? "Sept online hours > rows, dupes, one variable at bivariate, with evidence
Is it mix? Data? Jan–Aug, all regions" missing, ranges a time segments, time and confidence
EDA LOG | step | question | method (cell) | finding + number + n | confidence | next step |
STOP when: every question answered (or "cannot answer") · hypotheses ranked · time box reached
Alt text: A left-to-right flow: a vague concern becomes a question tree with branches for whether it is real, where it happens, whether it is a mix effect and whether it is a data problem; then testable hypotheses; then data checks of rows, duplicates, missing values and ranges; then a univariate pass of one variable at a time; then bivariate, segment and time views in later lessons; ending in ranked findings with evidence and confidence. Below, the columns of an EDA log: step, question, method with the cell, finding with a number and n, confidence, next step. The stopping rules: every question answered or marked unanswerable, hypotheses ranked, or the time box reached.
7. Guided Practice
Open L57_starter.ipynb. The concern: "Online orders feel slower this month (September)."
- Write a question tree (at least four leaves, including "is it the mix?" and "is it the data?").
- Write two testable hypotheses.
- Run the data-readiness checks of Part D and log the result.
- Profile
delivery_hoursfor online orders (web and app) in September and in January–August separately, and the channel counts. Log each step with a number and its n. - Write the next step for each question that is still open.
- Run Restart Kernel and Run All Cells….
8. Independent Practice
Repeat purchases at Harbor. The Harbor store manager writes: "Customers who started with us this summer don't seem to come back like before."
- Write an EDA plan: question tree, at least three testable hypotheses, the data checks you need, and your stopping rule. Define "come back" precisely and say what you will do about customers who have had too little time to come back.
- Run the checks and a first univariate pass of the variables your plan needs.
- Keep an EDA log of at least six steps, each with a number and its n.
- End with the two most promising next steps.
Work in L57_starter.ipynb (Part 2).
9. Knowledge Check — Structured EDA
Q1. What is the main output of EDA?
- A) A cleaned dataset
- B) A predictive model
- C) Findings with evidence and testable hypotheses about the business question
- D) As many charts as possible
Q2. Which is a testable hypothesis?
- A) "Something is wrong with online delivery"
- B) "Customers are unhappy"
- C) "Delivery could be better"
- D) "September online orders took longer to deliver than January–August online orders, in every region"
Q3. Why include "Is it the data?" in a question tree?
- A) Data are never wrong
- B) To make the tree longer
- C) A pipeline or data problem can create an apparent change; it must be ruled out first
- D) Because U09 requires it
Q4. What does a univariate pass look at?
- A) One variable at a time: distribution, centre, spread, oddities
- B) Only categorical variables
- C) Relationships between pairs of variables
- D) Forecasts
Q5. What should every EDA log entry contain?
- A) A chart
- B) The analyst's name
- C) The question, method, finding with a number and n, confidence, and next step
- D) Only the conclusion
Q6. During EDA you find two duplicate order IDs. What do you do in L57?
- A) Ignore them
- B) Log the check result; decide whether it affects the question; repairs belong to the cleaning process (U09)
- C) Stop the analysis permanently
- D) Delete them and continue silently
Q7. Which is a sensible stopping rule?
- A) Stop when every question is answered or marked unanswerable, hypotheses are ranked, or the time box is reached
- B) Stop when you have 30 charts
- C) Stop when the manager is happy
- D) Never stop
Q8. A manager feels "Station is slow", but Station's median delivery time is lower than two other points. What is the right next step?
- A) Stop the EDA
- B) Delete Station's data
- C) Tell the manager they are wrong
- D) Log the finding and ask what "slow" means (expectations, a few very late orders, promised times)
10. AI in Practice (optional, bounded)
Goal: ask an AI assistant to propose EDA questions; keep only those you can answer with the available data and that serve the business question.
- Give it the concern ("Harbor customers who started this summer don't come back") and the column lists of both files, and ask:
"Propose EDA questions to investigate this concern."
- For each proposed question, mark: answerable with these columns (yes/no) and serves the concern (yes/no). Keep only yes/yes.
Boundaries:
- AI is optional.
- Share the concern and column names, not customer rows.
- Questions that need data you do not have (customer satisfaction scores, competitor prices) are not useless, but they belong in "next steps", not in this analysis.
11. AI Verification
- Answerable? Which AI questions need columns you don't have?
- Relevant? Which questions drift away from Harbor or from repeat purchases?
- Missing branches: did it include "is it the data?" and "is it the mix?" (for example, more new or promo customers this summer)?
- Write down the questions you kept and why.
12. Business Lens
A structured investigation answers faster. A manager who receives "here is what we checked, here is what we found with numbers, here is what we will check next" can act or wait sensibly. A manager who receives thirty charts has to do the analysis themselves.
The EDA log is also protection: when someone later asks "did you look at distance?", the answer is a log line with a number, not a memory.
13. Workplace Task — The Two-Line Update
Fictional business: Cedar Lane. After your Guided Practice, the online manager asks for a quick update before you continue.
Write a two-line update (40–60 words): what you have confirmed so far (with one number and its n), and what you will check next. Do not claim a cause.
14. Mini Challenge — The Chart Pile
Fictional business: Cedar Lane. A colleague shows you twelve charts they made "to understand delivery times": revenue by month, basket value histogram, refunds by category, orders by weekday, and eight others, but no question.
print(orders.columns.tolist())
- Write the question the colleague should have started from (pick one plausible concern about delivery times).
- Build a four-leaf question tree for it.
- From the twelve chart topics listed above, say which ones serve your tree, and which do not.
- Write one sentence your colleague can keep as a rule.
Submit before feedback. No solution is provided here.
15. Resources
Required (about 30 min):
- NIST/SEMATECH e-Handbook of Statistical Methods, Chapter 1: Exploratory Data Analysis (PDF), sections 1.1 (what EDA is and how it differs from classical analysis). https://itl.nist.gov/div898/handbook/toolaids/pff/eda.pdf (Opened 20 Oct 2026.)
Recommended:
- AWS Cloud Adoption Framework — Business Perspective: Data science, on starting from business questions. https://docs.aws.amazon.com/whitepapers/latest/aws-caf-business-perspective/data-science.html
- Practical Statistics for Data Scientists, 2nd ed., chapter 1 on exploratory data analysis (paid; optional). https://www.oreilly.com/library/view/practical-statistics-for/9781492072935/
16. Person to Follow / Role Model
NIST/SEMATECH e-Handbook authors (organisational role example, mapped by D.World for L57–L61).
- Verified fact: they publish the e-Handbook of Statistical Methods, whose Chapter 1 sets out exploratory data analysis and its graphical techniques (D.World Stage 3 map).
- Why D.World recommends it: a free, careful reference on how to explore data before drawing conclusions.
- Honest note: an organisation's publication; we cite only the fact above.
- Try this: in Chapter 1, find the list of EDA goals. Which one matches "is it the data?"
17. Glossary
| Term | Meaning in this lesson |
|---|---|
| Exploratory data analysis | Investigating data before deciding or modelling, to find patterns, problems and hypotheses |
| Question tree | A concern broken into specific, answerable questions |
| Hypothesis | A specific statement that data could contradict |
| EDA log | A running record: question, method, finding with number and n, confidence, next step |
| Univariate analysis | Looking at one variable at a time |
Review: validation checks (U09) · grouped KPIs (L27) · describing distributions (L52).
18. Reflection
- Which branch of a question tree do you most often forget?
- How did writing hypotheses first change what you looked at?
- What would your stopping rule be for a two-hour investigation?
19. Next Step
You can now plan an investigation and do the first pass. Next:
- L58 — Univariate & Bivariate Exploration. Compare groups and pairs of variables with the right view, and state every finding with its n.
20. Assessment Evidence
| Evidence | What it shows | How it is judged |
|---|---|---|
| Harbor EDA plan + log + univariate pass (Independent Practice) | Question tree with mix and data branches; precise definition; censoring handled; checks; ≥ 6 log steps with numbers and n | Rubric L57-IP |
| Guided practice (online delivery concern) | Tree, hypotheses, checks, profiles, log | Rubric L57-GP |
| Knowledge check (Q1–Q8) | EDA purpose, hypotheses, checks, logging, stopping | 7 or more of 8 correct |
| Workplace task and mini challenge | Honest update; questions before charts | Rubrics L57-WT, L57-MC |
References
- NIST/SEMATECH. e-Handbook of Statistical Methods, Chapter 1: Exploratory Data Analysis. https://itl.nist.gov/div898/handbook/toolaids/pff/eda.pdf (NIST-EDA). Supports: purpose and approach of EDA; role example.
- Bruce, P., Bruce, A., Gedeck, P. Practical Statistics for Data Scientists, 2nd ed. (PSDS). Supports: EDA (optional).
- Amazon Web Services. AWS CAF — Business Perspective: Data science. https://docs.aws.amazon.com/whitepapers/latest/aws-caf-business-perspective/data-science.html (R05). Supports: starting from business questions.
- D.World Stage 3 Detailed Lesson Map v0.9 (L57 row). Internal.
L57 — التحليل الاستكشافي المنظَّم: من سؤال العمل إلى خطة التحليل
ملف المتعلّم. الإجابات والحلول النموذجية في ملف منفصل خاص بالمدرّس فقط. الإصدار: U14 Production v2 · تاريخ المحتوى: 20 أكتوبر 2026. تقديرات الوقت تقديرات للتخطيط إلى أن يختبرها المتعلّمون. الكود في هذا الدرس شُغّل باستخدام تجهيزات الدورة من L21 (Python 3.13 وpandas 3.0 وNumPy 2.5؛ وSciPy 1.18 وMatplotlib 3.11 حيث استُخدمتا) على Linux وعلى Windows 11 في 4 نوفمبر 2026 (راجع تقرير اختبار الكود Code Test Report).
كل الأنشطة التجارية خيالية، وكل البيانات مصطنعة. والرسوم تُرسم في دفترك؛ أما هذه الصفحة فتعرض الأرقام التي يطبعها الكود نفسه.
1. بطاقة الدرس
| البند | القيمة |
|---|---|
| العنوان | التحليل الاستكشافي المنظَّم: من سؤال العمل إلى خطة التحليل |
| المرحلة | المرحلة 3 — تحليل البيانات |
| الوحدة | U14 — تحليل البيانات الاستكشافي |
| رقم الدرس | L57 |
| المهارات | الغرض من التحليل الاستكشافي (EDA)؛ أشجار الأسئلة؛ الفرضيات القابلة للاختبار؛ فحوص جاهزية البيانات؛ التوصيف أحادي المتغير؛ سجل التحليل الاستكشافي؛ قواعد التوقف |
| المستوى | متوسط |
| الوقت المتوقع | حوالي ساعتين (تقدير للتخطيط، غير مُتحقَّق منه) |
| مستوى التقييم | تدريب / تطبيق |
| المتطلبات السابقة | U09 (فحوص التحقق)، وL27 (مؤشرات الأداء المجمّعة)، وL52 (وصف التوزيعات)، وL56 (الارتباط ليس سببًا). |
تفصيل الوقت (تقدير للتخطيط):
- المفهوم والمثال المحلول: 45 دقيقة.
- التدريب الموجَّه: 25 دقيقة.
- التدريب المستقل: 25 دقيقة.
- اختبار الفهم: 10 دقائق.
- الذكاء الاصطناعي في التطبيق والتحقق منه: 5 دقائق (اختياري).
- مهمة العمل والتحدي القصير: 10 دقائق.
ما تحتاج إليه: بيئتك من U13 ومجلد practice_files (L57_starter.ipynb؛ وفي u14_data: orders_2026.csv وcustomers_2026.csv).
بيانات U14. في orders_2026.csv صف واحد لكل طلب في Cedar Lane من يناير إلى سبتمبر 2026 (تاريخ الطلب، والمنطقة، ونقطة الاستلام، والقناة، والفئة الرئيسية، وقيمة السلة، والمسافة، وساعات التوصيل، وعلامة تأخير للتوصيل الذي يتجاوز 48 ساعة، وعلامة استرداد). وفي customers_2026.csv صف واحد لكل عميل (المنطقة، ونقطة الاستلام، وقناة التسجيل، وهل يستخدم التطبيق، والمسافة، وتفاصيل الطلب الأول، وهل طلب مرة أخرى خلال 90 يومًا من طلبه الأول، وعمودان ملخِّصان للفترة كلها). وتستخدم دروس U14 الخمسة كلها هذين الملفين.
2. هدف التعلّم
في نهاية هذا الدرس ستكون قادرًا على أن:
- تحوّل قلقًا غامضًا في العمل إلى خطة تحليل استكشافي: شجرة أسئلة، وفرضيات قابلة للاختبار، وفحوص البيانات التي تحتاج إليها؛
- تجري فحوص جاهزية البيانات وجولة أولى أحادية المتغير؛
- تحتفظ بسجل تحليل استكشافي يربط كل نتيجة بالكود وبالرقم الذي يقف خلفها.
الهدف المعتمد من D.World (بنصّه): Turn a vague business concern into an EDA plan with questions, hypotheses, required data checks and an EDA log, and complete the first univariate pass.
3. لماذا يهمّك هذا؟
نادرًا ما يصل القلق على هيئة سؤال. بل يصل على هيئة شعور: "online orders feel slower this month" (أي: "الطلبات الإلكترونية تبدو أبطأ هذا الشهر")، و"Harbor customers don't come back like they used to" (أي: "عملاء Harbor لا يعودون كما كانوا"). وأسهل رد هو أن تفتح البيانات وتبدأ برسم المخططات. وبعد ساعتين يكون عندك ثلاثون مخططًا، ولا إجابة، ومدير ما زال يشعر بالشيء نفسه.
تحليل البيانات الاستكشافي (exploratory data analysis، EDA) هو التحقيق الذي تجريه قبل أن تقرر أو تبني نموذجًا: ماذا تقول البيانات عن هذا القلق، وأين يجب أن ننظر بعد ذلك؟ وإذا أُحسن تنفيذه كان منظَّمًا. فأنت تكتب ما تريد معرفته، وتتحقق من أن البيانات صالحة للإجابة، وتنظر إلى شيء واحد في كل مرة، وتسجّل ما وجدته ومدى ثقتك به.
المبدأ الأساسي: التحليل الاستكشافي يبدأ من سؤال مكتوب وينتهي بنتائج مسجّلة مدعومة بالأدلة؛ ويجب أن يخدم كل مخطط سؤالًا في الخطة.
4. المفهوم
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
orders = pd.read_csv("u14_data/orders_2026.csv", parse_dates=["order_date"])
customers = pd.read_csv("u14_data/customers_2026.csv")
print(orders.shape, customers.shape)
print(orders.dtypes.astype(str).to_dict())
(4812, 13) (2198, 12)
{'order_id': 'str', 'customer_id': 'str', 'order_date': 'datetime64[us]', 'weekday': 'str', 'region': 'str', 'pickup_point': 'str', 'channel': 'str', 'main_category': 'str', 'basket_value': 'float64', 'distance_km': 'float64', 'delivery_hours': 'float64', 'late': 'int64', 'refunded': 'int64'}
الجزء A — ما الغرض من التحليل الاستكشافي
التحليل الاستكشافي يحقق في البيانات قبل أن تقرر أو تختبر أو تبني نموذجًا. ومخرجاته ليست مخططات؛ بل نتائج (findings) ("استغرقت عمليات التوصيل الإلكترونية في سبتمبر حوالي 9 ساعات أكثر من الأشهر السابقة، n = …") وفرضيات (hypotheses) تستحق الفحص. وهو يختلف عن:
- تنظيف البيانات (U09): التحليل الاستكشافي يتحقق فقط من أن البيانات صالحة للاستكشاف؛ ولا يصلحها؛
- اختبار الفرضيات (L55): التحليل الاستكشافي يقترح فرضيات؛ ويمكن لاختبار أو تجربة أن يؤكد إحداها لاحقًا؛
- العرض (U15): مخططات التحليل الاستكشافي مناظر عمل؛ والمخططات المصقولة تأتي لاحقًا.
الجزء B — شجرة الأسئلة
شجرة الأسئلة (question tree) تفكك قلقًا غامضًا إلى أسئلة محددة تستطيع البيانات الإجابة عنها. ابدأ بالقلق في الأعلى؛ وكل فرع يسأل "ما الذي يجب أن يكون صحيحًا؟":
"Online orders feel slower this month"
├── Is it real? Q1 Are online delivery hours higher in September than before? (how much, for how many orders)
├── Where? Q2 Is it all regions / pickup points, or some?
│ Q3 Is it web and app, or one channel?
├── Is it the mix? Q4 Did September have more long-distance or Monday orders (which are slower anyway)?
└── Is it the data? Q5 Are September's dates and hours complete and in the usual range?
(ترجمة الشجرة: "الطلبات الإلكترونية تبدو أبطأ هذا الشهر" — هل هو حقيقي؟ Q1 هل ساعات التوصيل الإلكتروني في سبتمبر أعلى مما قبله؟ (بكم، ولكم طلب). أين؟ Q2 هل في كل المناطق / نقاط الاستلام، أم في بعضها؟ Q3 هل في الموقع والتطبيق، أم في قناة واحدة؟ هل هو تأثير المزيج؟ Q4 هل كان في سبتمبر طلبات أكثر لمسافات طويلة أو أيام اثنين (وهي أبطأ أصلًا)؟ هل هي البيانات؟ Q5 هل تواريخ سبتمبر وساعاته مكتملة وفي المدى المعتاد؟)
كل ورقة في الشجرة يمكن الإجابة عنها بجدول أو رسم واحد. وفروع مثل "هل هو تأثير المزيج؟" و"هل هي البيانات؟" تحميك من أشيع الإجابات الخاطئة.
الجزء C — الفرضيات عبارات قابلة للاختبار
الفرضية في التحليل الاستكشافي عبارة محددة يمكن للبيانات أن تنقضها:
- ✘ "هناك خطأ ما في التوصيل الإلكتروني." (غير قابلة للاختبار.)
- ✔ "استغرق توصيل الطلبات الإلكترونية في سبتمبر وقتًا أطول من الطلبات الإلكترونية في يناير–أغسطس، في كل منطقة." (قابلة للاختبار: قارن حسب الشهر والمنطقة.)
- ✔ "الزيادة في سبتمبر يفسّرها ازدياد الطلبات لمسافات طويلة." (قابلة للاختبار: قارن ساعات التوصيل داخل فئات المسافة.)
اكتب الفرضيات قبل الاستكشاف، وأضف فرضيات جديدة أثناء العمل، مع تعليم وقت إضافة كل منها.
الجزء D — فحوص جاهزية البيانات
قبل الاستكشاف، تأكد من أن البيانات صالحة للسؤال. هذه هي فحوص U09، تُجرى بسرعة وتُسجَّل، وليست مشروع تنظيف:
checks = {
"rows": len(orders),
"duplicate order_id": int(orders["order_id"].duplicated().sum()),
"missing values (any column)": int(orders.isna().sum().sum()),
"date range": f"{orders['order_date'].min().date()} to {orders['order_date'].max().date()}",
"days with no orders": [d.date().isoformat() for d in pd.date_range(orders["order_date"].min(), orders["order_date"].max()).difference(orders["order_date"])],
"basket_value <= 0": int((orders["basket_value"] <= 0).sum()),
"delivery_hours range": f"{orders['delivery_hours'].min()} to {orders['delivery_hours'].max()}",
"late flag consistent with hours > 48": bool(((orders["delivery_hours"] > 48).astype(int) == orders["late"]).all()),
}
for k, v in checks.items():
print(f"{k:40} {v}")
rows 4812
duplicate order_id 0
missing values (any column) 0
date range 2026-01-01 to 2026-09-30
days with no orders ['2026-01-06']
basket_value <= 0 0
delivery_hours range 4.0 to 69.6
late flag consistent with hours > 48 True
لا تكرارات، ولا قيم مفقودة، والقيم في المدى، وعلامة التأخير تتفق مع الساعات. يوم واحد في التقويم ليس فيه أي طلبات. وهذه نتيجة يجب تسجيلها وفحصها (إغلاق؟ تحميل بيانات مفقود؟) قبل أن تشوّه مخططًا يوميًا؛ أما في سؤال عن وقت التوصيل الشهري فلا تغيّر الصورة. ولو أظهر أحد الفحوص فجوة كبيرة، لكانت النتيجة الأولى "مشكلة في البيانات"، ولتغيّر مسار التحقيق.
الجزء E — الجولة الأولى أحادية المتغير
التحليل أحادي المتغير (univariate analysis) ينظر إلى متغير واحد في كل مرة (L52): توزيعه، ومركزه، وانتشاره، وأي شيء غريب فيه. وصّف كل متغير يظهر في الخطة، لا كل عمود في الملف:
def profile(s):
"""Short profile of a numeric Series: n, missing, centre, spread, tails."""
return {"n": int(s.count()), "missing": int(s.isna().sum()), "mean": round(float(s.mean()), 2), "median": round(float(s.median()), 2),
"p10": round(float(s.quantile(0.10)), 2), "p90": round(float(s.quantile(0.90)), 2), "max": round(float(s.max()), 2)}
for col in ["delivery_hours", "distance_km", "basket_value"]:
print(col, profile(orders[col]))
print(orders["channel"].value_counts().to_dict())
(نص التوثيق داخل الدالة: توصيف قصير لـ Series رقمية: العدد، والمفقود، والمركز، والانتشار، والذيلان.)
delivery_hours {'n': 4812, 'missing': 0, 'mean': 33.1, 'median': 32.8, 'p10': 20.4, 'p90': 45.79, 'max': 69.6}
distance_km {'n': 4812, 'missing': 0, 'mean': 5.99, 'median': 6.0, 'p10': 1.8, 'p90': 10.0, 'max': 14.0}
basket_value {'n': 4812, 'missing': 0, 'mean': 22.8, 'median': 20.15, 'p10': 10.39, 'p90': 38.09, 'max': 101.1}
{'web': 2097, 'store': 1575, 'app': 1140}
في الفئات، تكون الأعداد (والحصص) هي التوصيف أحادي المتغير. دوّن ما تراه في السجل، ثم انتقل: فالمقارنة عبر الأشهر والمناطق هي الخطوة التالية (L58).
الجزء F — سجل التحليل الاستكشافي
سجل التحليل الاستكشافي (EDA log) جدول متجدد: صف لكل خطوة، فيه السؤال، والطريقة، والنتيجة (مع رقم وعدد المشاهدات n)، ومدى ثقتك، والخطوة التالية. وفي الدفتر تصلح قائمة من القواميس تُحوَّل إلى DataFrame:
log = []
def log_step(question, method, finding, confidence, next_step):
log.append({"step": len(log) + 1, "question": question, "method": method, "finding": finding,
"confidence": confidence, "next_step": next_step})
log_step("Q5 data fit?", "readiness checks (Part D)", "no duplicates/missing; 1 day with no orders (logged, to check); flags consistent",
"high", "profile key variables")
log_step("profile", "profile() on delivery_hours", f"median {orders['delivery_hours'].median():.1f} h, p90 {orders['delivery_hours'].quantile(0.9):.1f} h, n={len(orders)}",
"high", "compare by month and channel (Q1, Q3)")
print(pd.DataFrame(log).to_string(index=False))
step question method finding confidence next_step
1 Q5 data fit? readiness checks (Part D) no duplicates/missing; 1 day with no orders (logged, to check); flags consistent high profile key variables
2 profile profile() on delivery_hours median 32.8 h, p90 45.8 h, n=4812 high compare by month and channel (Q1, Q3)
كل نتيجة تحمل رقمًا والخلية التي أنتجته. والثقة حكم تقديري: "high" (عالية) لعدّ مباشر، و"medium" (متوسطة) لنمط في مجموعات صغيرة، و"low" (منخفضة) لحدس.
الجزء G — قواعد التوقف
يمكن للتحليل الاستكشافي أن يستمر إلى ما لا نهاية. فقرّر في الخطة متى ستتوقف:
- لكل سؤال في الشجرة إجابة (أو عُلّم بأنه "لا يمكن الإجابة عنه بهذه البيانات")؛
- الفرضيات الأهم مرتّبة مع الأدلة المؤيدة والمعارضة؛
- بلغت مهلة زمنية محددة (مثل ساعتين)، وكُتب ما تبقّى على هيئة خطوات تالية.
والرسم العشوائي للمخططات هو عكس قاعدة التوقف: إذا لم يُجب مخطط عن سؤال في الشجرة، فلا ترسمه (أو أضف السؤال أولًا).
5. مثال محلول
Cedar Lane (نشاط خيالي)، بيانات مصطنعة. يقول مدير Station: "Customers keep telling us Station deliveries take too long." (أي: "العملاء يكررون لنا أن توصيلات Station تستغرق وقتًا طويلًا جدًا.")
الخطوة 1 — شجرة الأسئلة.
"Station deliveries take too long"
├── Q1 Are Station delivery hours higher than other pickup points? (median, p90, share late)
├── Q2 Is it all of 2026 or recent? (L58/L60 — next step)
└── Q3 Is the data fit? (Station rows present every month; hours in range)
(ترجمة الشجرة: "توصيلات Station تستغرق وقتًا طويلًا جدًا" — Q1 هل ساعات توصيل Station أعلى من نقاط الاستلام الأخرى؟ (الوسيط، وP90، وحصة المتأخر). Q2 هل هذا في 2026 كلها أم مؤخرًا؟ (L58/L60 — الخطوة التالية). Q3 هل البيانات صالحة؟ (صفوف Station موجودة كل شهر؛ والساعات في المدى).)
الخطوة 2 — الفرضية (مكتوبة قبل الاستكشاف): "توصيلات Station أبطأ من نقطة الاستلام المتوسطة بأكثر من ساعتين عند الوسيط."
الخطوة 3 — فحوص البيانات لهذا السؤال.
sta = orders[orders["pickup_point"] == "PK-STA"]
print("Station orders:", len(sta), "| months present:", sta["order_date"].dt.month.nunique(), "of 9")
print("Station hours range:", sta["delivery_hours"].min(), "to", sta["delivery_hours"].max())
Station orders: 644 | months present: 9 of 9
Station hours range: 9.8 to 55.4
الخطوة 4 — مقارنة أحادية المتغير.
summary = orders.groupby("pickup_point")["delivery_hours"].agg(n="count", median="median", p90=lambda s: s.quantile(0.9))
summary["late_share"] = orders.groupby("pickup_point")["late"].mean()
print(summary.round(2).to_string())
n median p90 late_share
pickup_point
PK-CEN 935 24.90 34.90 0.00
PK-HAR 968 30.90 42.10 0.03
PK-NOR 978 37.10 46.86 0.08
PK-STA 644 28.05 37.80 0.01
WEB-HOME 1287 39.50 50.20 0.15
الخطوة 5 — السجل والخلاصة. وسيط وقت التوصيل في Station ليس الأعلى: فنقطة التوصيل المنزلي (WEB-HOME) وNorth أبطأ، لأن عملاءهما يسكنون أبعد (والمسافة تفسير مرجّح سيُفحص في L58). فالفرضية غير مدعومة بالنسبة إلى Station.
مدخل السجل: Q1 · groupby للوسيط وP90 وحصة المتأخر حسب نقطة الاستلام · وسيط Station حوالي 28 ساعة مقابل 37–40 ساعة لـ NOR وWEB-HOME؛ وحصة المتأخر في Station حوالي 1% (n = 644) · الثقة عالية · التالي: اسأل ماذا يقصد العملاء بـ "وقت طويل جدًا" (التوقعات مقابل الواقع؟)، وقارن بالمواعيد الموعودة.
هذه النتيجة تغيّر الحوار: فقد تكون الشكوى متعلقة بالتوقعات أو ببعض الطلبات المتأخرة جدًا، لا بوقت التوصيل المعتاد.
6. شرح بصري
VAGUE CONCERN ──▶ QUESTION TREE ──▶ HYPOTHESES (testable) ──▶ DATA CHECKS ──▶ UNIVARIATE PASS ──▶ (L58–L61) ──▶ RANKED FINDINGS
"feels slower" Is it real? Where? "Sept online hours > rows, dupes, one variable at bivariate, with evidence
Is it mix? Data? Jan–Aug, all regions" missing, ranges a time segments, time and confidence
EDA LOG | step | question | method (cell) | finding + number + n | confidence | next step |
STOP when: every question answered (or "cannot answer") · hypotheses ranked · time box reached
وصف الرسم: تدفق من اليسار إلى اليمين: قلق غامض يصبح شجرة أسئلة بفروع تسأل هل هو حقيقي، وأين يحدث، وهل هو تأثير مزيج، وهل هو مشكلة بيانات؛ ثم فرضيات قابلة للاختبار؛ ثم فحوص بيانات للصفوف والتكرارات والقيم المفقودة والمدى؛ ثم جولة أحادية المتغير لمتغير واحد في كل مرة؛ ثم مناظر ثنائية المتغير وللشرائح وللزمن في الدروس اللاحقة؛ وتنتهي بنتائج مرتّبة مع الأدلة ومستوى الثقة. وفي الأسفل أعمدة سجل التحليل الاستكشافي: الخطوة، والسؤال، والطريقة مع الخلية، والنتيجة مع رقم وn، والثقة، والخطوة التالية. وقواعد التوقف: كل سؤال أُجيب عنه أو عُلّم بأنه لا يمكن الإجابة عنه، والفرضيات مرتّبة، أو بلغت المهلة الزمنية. (نصوص الرسم: VAGUE CONCERN = قلق غامض؛ QUESTION TREE = شجرة الأسئلة؛ HYPOTHESES (testable) = فرضيات (قابلة للاختبار)؛ DATA CHECKS = فحوص البيانات؛ UNIVARIATE PASS = جولة أحادية المتغير؛ RANKED FINDINGS = نتائج مرتّبة؛ STOP when = توقف عندما.)
7. تدريب موجَّه
افتح L57_starter.ipynb. القلق: "Online orders feel slower this month (September)." (أي: "الطلبات الإلكترونية تبدو أبطأ هذا الشهر (سبتمبر).")
- اكتب شجرة أسئلة (أربع أوراق على الأقل، منها "هل هو تأثير المزيج؟" و"هل هي البيانات؟").
- اكتب فرضيتين قابلتين للاختبار.
- أجرِ فحوص جاهزية البيانات من الجزء D وسجّل النتيجة.
- وصّف
delivery_hoursللطلبات الإلكترونية (الموقع والتطبيق) في سبتمبر وفي يناير–أغسطس كلًّا على حدة، وأعداد القنوات. وسجّل كل خطوة مع رقم وعدد المشاهدات n. - اكتب الخطوة التالية لكل سؤال ما زال مفتوحًا.
- شغّل Restart Kernel and Run All Cells….
8. تدريب مستقل
الشراء المتكرر في Harbor. يكتب مدير متجر Harbor: "Customers who started with us this summer don't seem to come back like before." (أي: "العملاء الذين بدؤوا التعامل معنا هذا الصيف لا يبدو أنهم يعودون كما كان يحدث من قبل.")
- اكتب خطة تحليل استكشافي: شجرة أسئلة، وثلاث فرضيات قابلة للاختبار على الأقل، وفحوص البيانات التي تحتاج إليها، وقاعدة التوقف. وعرّف "يعود" تعريفًا دقيقًا، واذكر ماذا ستفعل بالعملاء الذين لم يمضِ عليهم وقت كافٍ ليعودوا.
- أجرِ الفحوص وجولة أولى أحادية المتغير على المتغيرات التي تحتاج إليها خطتك.
- احتفظ بسجل تحليل استكشافي من ست خطوات على الأقل، لكل منها رقم وعدد المشاهدات n.
- اختم بأكثر خطوتين تاليتين واعدتين.
اعمل في L57_starter.ipynb (الجزء 2).
9. اختبار الفهم (Knowledge Check) — التحليل الاستكشافي المنظَّم
Q1. ما المخرج الرئيسي للتحليل الاستكشافي؟
- A) مجموعة بيانات منظّفة
- B) نموذج تنبؤي
- C) نتائج مدعومة بالأدلة وفرضيات قابلة للاختبار حول سؤال العمل
- D) أكبر عدد ممكن من المخططات
Q2. أي مما يلي فرضية قابلة للاختبار؟
- A) "هناك خطأ ما في التوصيل الإلكتروني"
- B) "العملاء غير راضين"
- C) "التوصيل يمكن أن يكون أفضل"
- D) "استغرق توصيل الطلبات الإلكترونية في سبتمبر وقتًا أطول من الطلبات الإلكترونية في يناير–أغسطس، في كل منطقة"
Q3. لماذا نضمّن سؤال "هل هي البيانات؟" في شجرة الأسئلة؟
- A) البيانات لا تكون خاطئة أبدًا
- B) لنجعل الشجرة أطول
- C) مشكلة في خط المعالجة أو في البيانات قد تصنع تغيّرًا ظاهريًا؛ ويجب استبعادها أولًا
- D) لأن U09 تشترط ذلك
Q4. إلامَ تنظر الجولة أحادية المتغير؟
- A) متغير واحد في كل مرة: التوزيع، والمركز، والانتشار، والغرائب
- B) المتغيرات الفئوية فقط
- C) العلاقات بين أزواج المتغيرات
- D) التنبؤات
Q5. ماذا يجب أن يحتوي كل مدخل في سجل التحليل الاستكشافي؟
- A) مخططًا
- B) اسم المحلل
- C) السؤال، والطريقة، والنتيجة مع رقم وn، والثقة، والخطوة التالية
- D) الخلاصة فقط
Q6. أثناء التحليل الاستكشافي وجدت رقمي طلب مكرّرين. ماذا تفعل في L57؟
- A) تتجاهلهما
- B) تسجّل نتيجة الفحص؛ وتقرر هل يؤثر ذلك في السؤال؛ والإصلاح من شأن عملية التنظيف (U09)
- C) توقف التحليل نهائيًا
- D) تحذفهما وتكمل بصمت
Q7. أي مما يلي قاعدة توقف معقولة؟
- A) توقف عندما يُجاب عن كل سؤال أو يُعلَّم بأنه لا يمكن الإجابة عنه، أو تُرتَّب الفرضيات، أو تبلغ المهلة الزمنية
- B) توقف عندما يصبح لديك 30 مخططًا
- C) توقف عندما يكون المدير راضيًا
- D) لا تتوقف أبدًا
Q8. يشعر مدير بأن "Station بطيئة"، لكن وسيط وقت التوصيل في Station أقل من نقطتين أخريين. ما الخطوة التالية الصحيحة؟
- A) أوقف التحليل الاستكشافي
- B) احذف بيانات Station
- C) أخبر المدير أنه مخطئ
- D) سجّل النتيجة واسأل ماذا تعني "بطيئة" (التوقعات، أو بعض الطلبات المتأخرة جدًا، أو المواعيد الموعودة)
10. الذكاء الاصطناعي في التطبيق (اختياري، وفي حدود)
الهدف: أن تطلب من مساعد ذكاء اصطناعي اقتراح أسئلة للتحليل الاستكشافي؛ ولا تُبقِ إلا ما تستطيع الإجابة عنه بالبيانات المتاحة وما يخدم سؤال العمل.
- أعطه القلق ("عملاء Harbor الذين بدؤوا هذا الصيف لا يعودون") وقوائم أعمدة الملفين، واسأل:
"Propose EDA questions to investigate this concern."
وبالعربية: "اقترح أسئلة تحليل استكشافي للتحقيق في هذا القلق."
- لكل سؤال مقترح، علّم: يمكن الإجابة عنه بهذه الأعمدة (نعم/لا) ويخدم القلق (نعم/لا). ولا تُبقِ إلا ما كان نعم/نعم.
الحدود:
- استخدام الذكاء الاصطناعي اختياري.
- شارك القلق وأسماء الأعمدة، لا صفوف العملاء.
- الأسئلة التي تحتاج إلى بيانات ليست لديك (درجات رضا العملاء، أسعار المنافسين) ليست عديمة الفائدة، لكن مكانها "الخطوات التالية"، لا هذا التحليل.
11. التحقق من مخرجات الذكاء الاصطناعي
- هل يمكن الإجابة عنها؟ أي أسئلة الذكاء الاصطناعي تحتاج إلى أعمدة ليست لديك؟
- هل هي ذات صلة؟ أي الأسئلة تبتعد عن Harbor أو عن الشراء المتكرر؟
- فروع ناقصة: هل ضمّن "هل هي البيانات؟" و"هل هو تأثير المزيج؟" (مثل ازدياد العملاء الجدد أو عملاء العروض هذا الصيف)؟
- دوّن الأسئلة التي أبقيتها ولماذا.
12. منظور الأعمال
التحقيق المنظَّم يجيب أسرع. فالمدير الذي يتلقى "هذا ما فحصناه، وهذا ما وجدناه بالأرقام، وهذا ما سنفحصه بعد ذلك" يستطيع أن يتصرف أو ينتظر بحكمة. أما المدير الذي يتلقى ثلاثين مخططًا فعليه أن يجري التحليل بنفسه.
وسجل التحليل الاستكشافي حماية أيضًا: فعندما يسأل أحدهم لاحقًا "هل نظرت إلى المسافة؟"، تكون الإجابة سطرًا في السجل فيه رقم، لا ذكرى.
13. مهمة عمل — التحديث من سطرين
نشاط خيالي: Cedar Lane. بعد تدريبك الموجَّه، يطلب مدير المتجر الإلكتروني تحديثًا سريعًا قبل أن تكمل.
اكتب تحديثًا من سطرين (من 40 إلى 60 كلمة): ما الذي تأكدت منه حتى الآن (مع رقم واحد وعدد المشاهدات n)، وما الذي ستفحصه بعد ذلك. ولا تدّعِ سببًا.
14. تحدٍّ قصير — كومة المخططات
نشاط خيالي: Cedar Lane. يعرض عليك زميل اثني عشر مخططًا رسمها "لفهم أوقات التوصيل": الإيرادات حسب الشهر، ومدرج تكراري لقيمة السلة، والمبالغ المستردة حسب الفئة، والطلبات حسب يوم الأسبوع، وثمانية أخرى، لكن دون أي سؤال.
print(orders.columns.tolist())
- اكتب السؤال الذي كان يجب أن يبدأ منه الزميل (اختر قلقًا واحدًا معقولًا بشأن أوقات التوصيل).
- ابنِ له شجرة أسئلة من أربع أوراق.
- من موضوعات المخططات الاثني عشر المذكورة أعلاه، اذكر أيها يخدم شجرتك، وأيها لا يخدمها.
- اكتب جملة واحدة يحتفظ بها زميلك كقاعدة.
سلّم عملك قبل أن تتلقى الملاحظات. لا يوجد حل منشور هنا.
15. المصادر
مطلوب (حوالي 30 دقيقة):
- NIST/SEMATECH e-Handbook of Statistical Methods, Chapter 1: Exploratory Data Analysis (PDF)، القسم 1.1 (ما التحليل الاستكشافي، وكيف يختلف عن التحليل الكلاسيكي). https://itl.nist.gov/div898/handbook/toolaids/pff/eda.pdf (فُتح في 20 أكتوبر 2026.)
موصى به:
- AWS Cloud Adoption Framework — Business Perspective: Data science، عن البدء من أسئلة العمل. https://docs.aws.amazon.com/whitepapers/latest/aws-caf-business-perspective/data-science.html
- Practical Statistics for Data Scientists, 2nd ed.، الفصل 1 عن تحليل البيانات الاستكشافي (مدفوع؛ اختياري). https://www.oreilly.com/library/view/practical-statistics-for/9781492072935/
16. شخصية تتابعها / قدوة
مؤلفو NIST/SEMATECH e-Handbook (نموذج مؤسسي يُحتذى، اختارته D.World للدروس L57–L61).
- حقيقة مُتحقَّق منها: ينشرون e-Handbook of Statistical Methods، الذي يعرض فصله الأول تحليل البيانات الاستكشافي وأساليبه البيانية (خريطة D.World للمرحلة 3).
- لماذا توصي به D.World: مرجع مجاني ودقيق عن كيفية استكشاف البيانات قبل استخلاص النتائج.
- ملاحظة صادقة: هذا منشور لمؤسسة؛ ولا نذكر إلا الحقيقة المذكورة أعلاه.
- جرّب هذا: في الفصل الأول، ابحث عن قائمة أهداف التحليل الاستكشافي. أيها يطابق "هل هي البيانات؟"
17. المصطلحات
| المصطلح | معناه في هذا الدرس |
|---|---|
| تحليل البيانات الاستكشافي (Exploratory data analysis) | التحقيق في البيانات قبل اتخاذ القرار أو بناء النموذج، لإيجاد الأنماط والمشكلات والفرضيات |
| شجرة الأسئلة (Question tree) | قلق مفكَّك إلى أسئلة محددة يمكن الإجابة عنها |
| الفرضية (Hypothesis) | عبارة محددة يمكن للبيانات أن تنقضها |
| سجل التحليل الاستكشافي (EDA log) | سجل متجدد: السؤال، والطريقة، والنتيجة مع رقم وn، والثقة، والخطوة التالية |
| التحليل أحادي المتغير (Univariate analysis) | النظر إلى متغير واحد في كل مرة |
للمراجعة: فحوص التحقق (U09) · مؤشرات الأداء المجمّعة (L27) · وصف التوزيعات (L52).
18. تأمّل
- أي فرع من شجرة الأسئلة تنساه أكثر من غيره؟
- كيف غيّرت كتابة الفرضيات أولًا ما نظرت إليه؟
- ماذا ستكون قاعدة توقفك لتحقيق مدته ساعتان؟
19. الخطوة التالية
أصبحت الآن قادرًا على تخطيط تحقيق وإجراء الجولة الأولى. والتالي:
- L58 — الاستكشاف أحادي المتغير وثنائي المتغير (Univariate & Bivariate Exploration). قارن المجموعات وأزواج المتغيرات بالمنظر المناسب، واذكر كل نتيجة مع عدد مشاهداتها n.
20. أدلة التقييم
| الدليل | ما الذي يُظهره | كيف يُقيَّم |
|---|---|---|
| خطة التحليل الاستكشافي لـ Harbor + السجل + الجولة أحادية المتغير (التدريب المستقل) | شجرة أسئلة فيها فرعا المزيج والبيانات؛ وتعريف دقيق؛ ومعالجة الملاحظات غير المكتملة (censoring)؛ وفحوص؛ و6 خطوات أو أكثر في السجل بأرقام وn | دليل التقييم L57-IP |
| التدريب الموجَّه (قلق التوصيل الإلكتروني) | الشجرة، والفرضيات، والفحوص، والتوصيفات، والسجل | دليل التقييم L57-GP |
| اختبار الفهم (Q1–Q8) | غرض التحليل الاستكشافي، والفرضيات، والفحوص، والتسجيل، والتوقف | 7 إجابات صحيحة أو أكثر من 8 |
| مهمة العمل والتحدي القصير | تحديث صادق؛ والأسئلة قبل المخططات | دليلا التقييم L57-WT وL57-MC |
المراجع
- NIST/SEMATECH. e-Handbook of Statistical Methods, Chapter 1: Exploratory Data Analysis. https://itl.nist.gov/div898/handbook/toolaids/pff/eda.pdf (NIST-EDA). يدعم: غرض التحليل الاستكشافي ونهجه؛ ونموذج القدوة.
- Bruce, P., Bruce, A., Gedeck, P. Practical Statistics for Data Scientists, 2nd ed. (PSDS). يدعم: التحليل الاستكشافي (اختياري).
- Amazon Web Services. AWS CAF — Business Perspective: Data science. https://docs.aws.amazon.com/whitepapers/latest/aws-caf-business-perspective/data-science.html (R05). يدعم: البدء من أسئلة العمل.
- D.World Stage 3 Detailed Lesson Map v0.9 (صف L57). وثيقة داخلية.
L58 · Univariate & Bivariate Exploration Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
L59 · Multivariate Views & Segmentation Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
L60 · Cohort & Time-Based Exploration Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
L61 · Anomalies & Root-Cause Investigation Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
U14 Exit Check — EDA Investigation Report Membersللمشتركين
The exit brief is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
تكليف نهاية الوحدة ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
U15 · Data Visualization & Storytellingتصوير البيانات وسرد القصة
Stage 3المرحلة 3Free: the first lesson. The other lessons, the exit brief and the practice files are for Starter and Membership.
مجاني: الدرس الأول. باقي الدروس وتكليف نهاية الوحدة وملفات التمرين لأصحاب باقة البداية والاشتراك.
Download U15 practice files (.zip, 49 KB)حمّل ملفات تمرين U15 (zip، 49 KB)
Unzip into a short folder such as D:\DWorld on Windows. The zip has only learner files: no answers.
فك الضغط في مجلد قصير مثل D:\DWorld على ويندوز. الملف يحتوي ملفات المتعلم فقط بدون إجابات.
L62 · Chart Grammar & Choosing the Right ChartL62 · قواعد بناء المخطط واختيار المخطط المناسب Freeمجاني
L62 — Chart Grammar & Choosing the Right Chart
Learner file. Answers and model solutions are in a separate instructor-only file. Version: U15 Production v2 · Content date: 20 October 2026. Time estimates are planning estimates until learners have tested them. The code in this lesson was run with the course setup from L21 (Python 3.13, pandas 3.0 and NumPy 2.5; SciPy 1.18 and Matplotlib 3.11 where used) on Linux and on Windows 11 on 4 November 2026 (see the Code Test Report).
All businesses are fictional and all data is synthetic. Charts are drawn in your notebook and saved as files; this page shows the checks printed by the same code.
1. Lesson Identity
| Field | Value |
|---|---|
| Title | Chart Grammar & Choosing the Right Chart |
| Stage | Stage 3 — Data Analysis |
| Unit | U15 — Visualization & Storytelling |
| Lesson ID | L62 |
| Skills | Aesthetic mappings; question-to-chart directory; Figure/Axes anatomy and the fig, ax interface; pandas .plot as a wrapper; titles, labels, units; saving PNG/SVG; reusable plotting functions |
| Difficulty | Intermediate |
| Estimated time | About 2.5 hours (planning estimate, unvalidated) |
| Assessment level | Practice |
| Prerequisites | L52 (first Matplotlib plots), U14 (EDA working plots). |
Time breakdown (planning estimate): concept and worked example 60 min · guided practice 30 min · independent practice 35 min · knowledge check 10 min · AI 5 min (optional) · workplace task and mini challenge 10 min.
What you need: practice_files (L62_starter.ipynb; u15_data/monthly_performance.csv: one row per month × region × category with orders, revenue, gross margin, deliveries and late deliveries, January–September 2026).
2. Learning Objective
By the end of this lesson, you will be able to:
- map data to position, length, colour and shape;
- choose a chart type from the question asked (amount, distribution, proportion, relationship, trend);
- build the chart with Matplotlib's explicit
fig, axinterface, with a title, axis labels and units, and save it.
D.World approved objective (verbatim): Map data to position, length, color and shape, choose a chart type from the question asked (amount, distribution, proportion, relationship, trend), and build it with Matplotlib's explicit interface.
3. Why It Matters
In U14 you drew working plots: quick views to help you think. A chart for a manager has a different job: it must answer one question quickly and correctly for someone who did not do the analysis. The most common failures are not technical: the wrong chart type for the question, no units, an unreadable default, a title that says nothing.
Choosing a chart is not a matter of taste. It follows from what the question is about (an amount, a trend, a distribution…) and from how accurately people read different visual encodings: positions along a common axis are read most accurately, colour shades least.
Key principle: start from the question, map the most important comparison to position or length, and label everything a reader needs (title, axes, units, source).
4. Concept
from pathlib import Path
import matplotlib
import matplotlib.pyplot as plt
import pandas as pd
perf = pd.read_csv("u15_data/monthly_performance.csv")
Path("figures").mkdir(exist_ok=True)
print(perf.shape, "| Matplotlib", matplotlib.__version__)
print(perf.head(3).to_string(index=False))
(180, 8) | Matplotlib 3.11.2
month region category orders revenue gross_margin deliveries late_deliveries
2026-01 CEN Pantry 52 1043.40 358.81 28 2
2026-01 CEN Drinks 45 1125.62 426.57 24 0
2026-01 CEN Fresh 36 584.97 151.29 19 1
Part A — Aesthetic mappings: the grammar of a chart
Every chart maps data values onto visual properties called aesthetics: position (x, y), length (bar height), colour, shape, size. This idea is the chart grammar: a chart is a set of mappings, not a named template. Readers judge some aesthetics more accurately than others:
- position along a common scale (most accurate);
- length (bars from a common baseline);
- angle and area (pies, bubbles);
- colour intensity and hue (least accurate for amounts; good for categories).
So the comparison that matters most gets position or length; colour is for grouping, not for reading values.
Part B — From question to chart: a directory
| Question type | Example question | Default chart |
|---|---|---|
| Amount (compare values across categories) | "Which region had the most revenue?" | sorted bar chart (horizontal if labels are long) |
| Distribution (how values are spread) | "How long do deliveries take?" | histogram or box plot (L52) |
| Proportion (parts of a whole) | "What share of revenue is Fresh?" | stacked bar or simple bar of shares; a pie only for 2–3 parts |
| Relationship (two numeric variables) | "Do farther deliveries take longer?" | scatter plot (L56) |
| Trend (change over time) | "How did revenue develop this year?" | line chart with time on x |
This chart directory is a starting point; L63 adds design rules for each family.
Part C — Figure and Axes: the explicit interface
A Matplotlib Figure is the whole image; an Axes is one plot area inside it, with its own x and y axis, title and labels. The object-oriented interface creates both explicitly and calls methods on them:
rev_by_region = perf.groupby("region")["revenue"].sum().sort_values()
fig, ax = plt.subplots(figsize=(7, 3.5))
bars = ax.barh(rev_by_region.index, rev_by_region.values / 1000)
ax.set_title("Online (WEB) brought the most revenue, January–September 2026")
ax.set_xlabel("Revenue (€ thousand)")
ax.set_ylabel("Region")
fig.tight_layout()
fig.savefig("figures/revenue_by_region.png", dpi=150)
print("bars:", len(bars), "| order (bottom to top):", list(rev_by_region.index))
print("x label:", ax.get_xlabel(), "| saved:", Path("figures/revenue_by_region.png").exists())
bars: 5 | order (bottom to top): ['STA', 'NOR', 'CEN', 'HAR', 'WEB']
x label: Revenue (€ thousand) | saved: True
fig, ax = plt.subplots(figsize=(width, height))sets the size in inches;ax.barhmaps region to position on y and revenue to length on x;- the title states the finding, the label states the unit;
fig.tight_layout()prevents cut-off labels;fig.savefigwrites the file (dpi=150for PNG;.svggives a vector file that scales without blurring).
Why the explicit interface? With several charts in one notebook, ax. calls make it unambiguous which plot you are changing, and the same code works inside functions.
Part D — pandas .plot is Matplotlib underneath
DataFrame.plot and Series.plot call Matplotlib for you and return an Axes. Pass ax= to keep control:
monthly = perf.groupby("month")["revenue"].sum() / 1000
fig, ax = plt.subplots(figsize=(7, 3))
returned = monthly.plot(ax=ax, marker="o")
ax.set_title("Revenue grew every month in 2026")
ax.set_xlabel("Month")
ax.set_ylabel("Revenue (€ thousand)")
print("pandas returned the same Axes:", returned is ax, "| points:", len(ax.lines[0].get_xdata()))
pandas returned the same Axes: True | points: 9
Use pandas plotting for quick charts; switch to plain ax. calls when you need full control (L63–L64).
Part E — A reusable plotting function
Charts you make more than once belong in a function that takes the data and an Axes and returns the Axes:
def bar_chart(ax, series, title, xlabel, scale=1.0):
"""Sorted horizontal bar chart of a Series (largest at the top)."""
s = (series / scale).sort_values()
ax.barh(s.index, s.values)
ax.set_title(title)
ax.set_xlabel(xlabel)
return ax
fig, ax = plt.subplots(figsize=(6, 3))
bar_chart(ax, perf.groupby("category")["gross_margin"].sum(), "Drinks and Pantry earn most gross margin", "Gross margin (€ thousand)", 1000)
fig.savefig("figures/margin_by_category.svg")
print("labels:", [t.get_text() for t in ax.get_yticklabels()])
labels: ['Household', 'Fresh', 'Pantry', 'Drinks']
The function rebuilds the same style every time, and reruns from a fresh kernel because it depends only on its inputs.
5. Worked Example
Cedar Lane (fictional), synthetic data. The operations director asks: "What share of our deliveries are late, by region?"
Step 1 — Question type. A proportion per category (late share by region) is an amount comparison of rates → sorted bar chart of percentages, starting at zero.
Step 2 — Compute the numbers first (the chart must match the table).
t = perf.groupby("region")[["late_deliveries", "deliveries"]].sum()
t["late_share"] = t["late_deliveries"] / t["deliveries"]
print(t.sort_values("late_share", ascending=False).round(4).to_string())
late_deliveries deliveries late_share
region
HAR 49 930 0.0527
NOR 33 748 0.0441
CEN 36 826 0.0436
STA 18 419 0.0430
WEB 113 2836 0.0398
Step 3 — Build, check, save.
s = (t["late_share"] * 100).sort_values()
fig, ax = plt.subplots(figsize=(6.5, 3))
ax.barh(s.index, s.values)
ax.set_xlim(0, max(s.values) * 1.15)
ax.set_title("About 1 in 20 deliveries is late in every region")
ax.set_xlabel("Late deliveries (% of deliveries), Jan–Sep 2026")
fig.tight_layout()
fig.savefig("figures/late_share_by_region.png", dpi=150)
print("x starts at", ax.get_xlim()[0], "| values match table:", [round(v, 2) for v in s.values] == [round(v * 100, 2) for v in t["late_share"].sort_values().values])
x starts at 0.0 | values match table: True
The differences between regions are small (about one percentage point between the highest and the lowest), and the title says so. A title such as "Late deliveries by region" would leave the reader to work that out, or to imagine a difference.
6. Visual Explanation
QUESTION ──▶ TYPE ──▶ CHART ──▶ MAPPING ──▶ LABELS ──▶ CHECK ──▶ SAVE
"Which region amount sorted region→y position title = finding numbers = table PNG (dpi) / SVG
sold most?" bars revenue→bar length axis labels+units axis from 0 (bars)
ACCURACY OF ENCODINGS: position > length > angle/area > colour (colour = groups, not amounts)
Figure (whole image) ⊃ Axes (one plot: x axis, y axis, title) fig, ax = plt.subplots(); ax.barh(...); ax.set_title(...)
Alt text: A left-to-right flow from a stakeholder question to its type, a chart, the mapping of data to visual properties, the labels, a check and saving. For the question which region sold most: amount, sorted bars, region mapped to y position and revenue to bar length, a title stating the finding, axis labels with units, numbers checked against the table, bars from zero, saved as PNG or SVG. Below, the ranking of encodings by reading accuracy: position, then length, then angle and area, then colour, which is for groups not amounts. A Figure contains Axes; the explicit interface creates both and calls methods on the Axes.
7. Guided Practice
Open L62_starter.ipynb. Rebuild three U14-style working plots as clean charts with fig, ax, a finding-stating title, axis labels with units, and a saved file each:
- Revenue by region (sorted horizontal bars, € thousand).
- Monthly revenue for all regions together (line chart, months on x).
- Monthly late share for WEB vs all other regions combined (two lines; label them).
- For each chart, print the numbers behind it and check them against the plotted values.
- Run Restart Kernel and Run All Cells… and confirm the three files are recreated.
8. Independent Practice
Five stakeholder questions. For each question, name the question type, the chart you would use, and one line explaining why (mapping and reading accuracy):
- "Which product category has the highest gross margin rate?"
- "How did Online (WEB) orders develop month by month compared with the four stores?"
- "What share of our revenue comes from each region?"
- "Is there a relationship between a region's order volume and its late share?" (use region × month points)
- "How are monthly revenues per region and category spread?"
Then build two of them (your choice) as finished charts: title stating the finding, axis labels with units, saved files, and a printed check that the plotted numbers match the table.
Work in L62_starter.ipynb (Part 2).
9. Knowledge Check — Chart Grammar
Q1. Which aesthetic do readers judge most accurately?
- A) Position along a common scale
- B) Area
- C) Colour hue
- D) Angle
Q2. "How did revenue develop this year?" Which chart?
- A) Pie chart
- B) Line chart with time on the x axis
- C) Scatter plot of revenue against region
- D) Histogram
Q3. "Which region had the most revenue?" Which chart?
- A) Box plot
- B) Pie chart with five slices
- C) Line chart
- D) Sorted bar chart
Q4. What is an Axes in Matplotlib?
- A) The whole image
- B) One plot area with its own axes, title and labels
- C) A saved file
- D) A colour map
Q5. What does revenue_series.plot(ax=ax) return?
- A) The Axes it drew on
- B) A DataFrame
- C) Nothing
- D) A new Figure
Q6. Which title is best for a chart of late share by region?
- A) "Chart 3"
- B) "Data"
- C) "About 1 in 20 deliveries is late in every region"
- D) "Late deliveries by region"
Q7. When is SVG a better file format than PNG?
- A) Never
- B) For smaller dpi
- C) For photos
- D) When the chart must scale without blurring (vector graphics)
Q8. What should colour encode in most business charts?
- A) Groups or categories (and emphasis), while amounts go on position or length
- B) Exact amounts
- C) Nothing
- D) The title
10. AI in Practice (optional, bounded)
Goal: ask an AI assistant to recommend a chart for a question; accept it only if it matches the question type in the directory and your data.
- Ask: "What chart should I use to show what share of revenue each of five regions brings?"
- Compare its answer with Part B.
Boundaries: AI is optional; share the question and column names only; a recommendation that ignores the number of categories or reading accuracy is not accepted as given.
11. AI Verification
- Type: did it identify a proportion question?
- Fit: does its chart suit five categories (a pie with five similar slices is hard to read)?
- Data: do you have the columns it assumes?
- Write down your final choice and why.
12. Business Lens
A chart is a decision aid. Managers spend seconds on a chart. A finding-stating title and the right chart type mean they read the answer, not the chart. Reusable plotting functions also make monthly reports consistent, so readers learn the format once.
13. Workplace Task — The Board Pack Chart
Fictional business: Cedar Lane. The finance lead needs one chart for the board pack: gross margin by category for January–September, with a title that states the finding.
- Build it (choose and justify the type), save it as SVG, and print the numbers behind it.
- Write a one-sentence caption with the source and period.
14. Mini Challenge — The Default Chart
Fictional business: Cedar Lane. A colleague's chart:
fig, ax = plt.subplots()
perf.groupby("region")["revenue"].sum().plot(kind="pie", ax=ax)
print([t.get_text() for t in ax.texts][:5])
- Before running it, list what a reader cannot easily see in this chart.
- Run it and rebuild it as a chart that answers "which region brings the most revenue, and by how much?"
- Add a title that states the finding and a unit.
- Write one sentence your colleague can keep as a rule.
Submit before feedback. No solution is provided here.
15. Resources
Required (about 40 min):
- Claus O. Wilke: Fundamentals of Data Visualization, "Directory of visualizations" and the chapter on mapping data onto aesthetics. https://clauswilke.com/dataviz/directory-of-visualizations.html (Opened 20 Oct 2026.)
- Matplotlib: Quick start guide, "Parts of a Figure" and "Coding styles" (explicit vs implicit). https://matplotlib.org/stable/users/explain/quick_start.html
Recommended:
- pandas: Chart visualization,
plotandax=. https://pandas.pydata.org/docs/user_guide/visualization.html
16. Person to Follow / Role Model
Claus O. Wilke (mapped by D.World for L62–L64).
- Verified fact: author of the web book Fundamentals of Data Visualization (D.World Stage 1 role models, R04, "deeper in Stage 3").
- Why D.World recommends him: the book explains chart choices through aesthetics and reading accuracy, with good and bad examples side by side.
- Honest note: we cite only the fact above.
- Try this: in the directory of visualizations, find the section on proportions. When does it suggest a pie chart?
17. Glossary
| Term | Meaning in this lesson |
|---|---|
| Aesthetic mapping | Assigning a data variable to a visual property (position, length, colour, shape) |
| Chart grammar | Thinking of a chart as a set of mappings rather than a named template |
| Figure | The whole Matplotlib image |
| Axes | One plot area in a Figure, with its own axes, title and labels |
| Object-oriented interface | Creating fig, ax explicitly and calling methods on them |
| Chart directory | A table from question type to a default chart |
Review: first plots (L52) · EDA working views (U14).
18. Reflection
- Which of your U14 plots would confuse a manager, and why?
- When would you break the directory's default?
- What will every chart title of yours state from now on?
19. Next Step
Next: L63 — Comparisons, Trends, Distributions & Relationships Done Well. Design rules for each chart family, including uncertainty.
20. Assessment Evidence
| Evidence | What it shows | How it is judged |
|---|---|---|
| Five chart choices + two built charts (Independent Practice) | Question types and charts justified; finished charts with titles, units, saved files, number checks | Rubric L62-IP |
| Guided practice (three rebuilt charts) | fig/ax interface; labels and units; checks; rerun | Rubric L62-GP |
| Knowledge check (Q1–Q8) | Encodings, directory, Figure/Axes, titles, formats | 7 or more of 8 correct |
| Workplace task and mini challenge | Board chart with caption; pie rebuilt as bars | Rubrics L62-WT, L62-MC |
References
- Wilke, C. O. Fundamentals of Data Visualization. https://clauswilke.com/dataviz/directory-of-visualizations.html (WILKE). Supports: aesthetics, directory; role-model fact.
- Matplotlib developers. Quick start guide. https://matplotlib.org/stable/users/explain/quick_start.html (MPL-QS). Supports: Figure/Axes, explicit interface, saving.
- pandas development team. Chart visualization. https://pandas.pydata.org/docs/user_guide/visualization.html (P-VIS). Supports:
.plotas a wrapper. - D.World Stage 3 Detailed Lesson Map v0.9 (L62 row). Internal.
L62 — قواعد بناء المخطط واختيار المخطط المناسب
ملف المتعلّم. الإجابات والحلول النموذجية في ملف منفصل خاص بالمدرّس فقط. الإصدار: U15 Production v2 · تاريخ المحتوى: 20 أكتوبر 2026. تقديرات الوقت تقديرات للتخطيط إلى أن يختبرها المتعلّمون. الكود في هذا الدرس شُغّل باستخدام تجهيزات الدورة من L21 (Python 3.13 وpandas 3.0 وNumPy 2.5؛ وSciPy 1.18 وMatplotlib 3.11 حيث استُخدمتا) على Linux وعلى Windows 11 في 4 نوفمبر 2026 (راجع تقرير اختبار الكود Code Test Report).
كل الأنشطة التجارية خيالية، وكل البيانات مصطنعة. والمخططات تُرسم في دفترك وتُحفظ ملفات؛ أما هذه الصفحة فتعرض الفحوص التي يطبعها الكود نفسه.
1. بطاقة الدرس
| البند | القيمة |
|---|---|
| العنوان | قواعد بناء المخطط واختيار المخطط المناسب |
| المرحلة | المرحلة 3 — تحليل البيانات |
| الوحدة | U15 — التصوير البياني ورواية القصص بالبيانات |
| رقم الدرس | L62 |
| المهارات | الربط الجمالي (aesthetic mappings)؛ دليل يربط السؤال بالمخطط؛ تشريح Figure وAxes وواجهة fig, ax؛ .plot في pandas كغلاف؛ العناوين وتسميات المحاور والوحدات؛ الحفظ بصيغة PNG/SVG؛ دوال رسم قابلة لإعادة الاستخدام |
| المستوى | متوسط |
| الوقت المتوقع | حوالي ساعتين ونصف (تقدير للتخطيط، غير مُتحقَّق منه) |
| مستوى التقييم | تدريب |
| المتطلبات السابقة | L52 (أول رسوم Matplotlib)، وU14 (الرسوم العملية في التحليل الاستكشافي). |
تفصيل الوقت (تقدير للتخطيط):
- المفهوم والمثال المحلول: 60 دقيقة.
- التدريب الموجَّه: 30 دقيقة.
- التدريب المستقل: 35 دقيقة.
- اختبار الفهم: 10 دقائق.
- الذكاء الاصطناعي: 5 دقائق (اختياري).
- مهمة العمل والتحدي القصير: 10 دقائق.
ما تحتاج إليه: مجلد practice_files (L62_starter.ipynb؛ وu15_data/monthly_performance.csv: صف واحد لكل شهر × منطقة × فئة، فيه الطلبات والإيرادات وهامش الربح الإجمالي وعدد التوصيلات والتوصيلات المتأخرة، من يناير إلى سبتمبر 2026).
2. هدف التعلّم
في نهاية هذا الدرس ستكون قادرًا على أن:
- تربط البيانات بالموضع والطول واللون والشكل؛
- تختار نوع المخطط من السؤال المطروح (كمية، أو توزيع، أو نسبة، أو علاقة، أو اتجاه زمني)؛
- تبني المخطط بواجهة
fig, axالصريحة في Matplotlib، بعنوان وتسميات للمحاور ووحدات، ثم تحفظه.
الهدف المعتمد من D.World (بنصّه): Map data to position, length, color and shape, choose a chart type from the question asked (amount, distribution, proportion, relationship, trend), and build it with Matplotlib's explicit interface.
3. لماذا يهمّك هذا؟
في U14 رسمت رسومًا عملية (working plots): نظرات سريعة تساعدك على التفكير. أما المخطط الموجَّه إلى مدير فوظيفته مختلفة: يجب أن يجيب عن سؤال واحد بسرعة وبدقة لشخص لم يُجرِ التحليل بنفسه. وأكثر الأخطاء شيوعًا ليست تقنية: نوع مخطط لا يناسب السؤال، أو غياب الوحدات، أو إعدادات افتراضية يصعب قراءتها، أو عنوان لا يقول شيئًا.
واختيار المخطط ليس مسألة ذوق. بل ينبع من موضوع السؤال (كمية، أو اتجاه زمني، أو توزيع…) ومن دقة قراءة الناس لأنواع الترميز البصري المختلفة: فالمواضع على محور مشترك تُقرأ بأعلى دقة، ودرجات اللون بأقلها.
المبدأ الأساسي: ابدأ من السؤال، واربط أهم مقارنة بالموضع أو الطول، وضع كل تسمية يحتاج إليها القارئ (العنوان، والمحاور، والوحدات، والمصدر).
4. المفهوم
from pathlib import Path
import matplotlib
import matplotlib.pyplot as plt
import pandas as pd
perf = pd.read_csv("u15_data/monthly_performance.csv")
Path("figures").mkdir(exist_ok=True)
print(perf.shape, "| Matplotlib", matplotlib.__version__)
print(perf.head(3).to_string(index=False))
(180, 8) | Matplotlib 3.11.2
month region category orders revenue gross_margin deliveries late_deliveries
2026-01 CEN Pantry 52 1043.40 358.81 28 2
2026-01 CEN Drinks 45 1125.62 426.57 24 0
2026-01 CEN Fresh 36 584.97 151.29 19 1
الجزء A — الربط الجمالي: قواعد بناء المخطط
كل مخطط يربط قيم البيانات بخصائص بصرية تُسمّى الخصائص الجمالية (aesthetics): الموضع (x وy)، والطول (ارتفاع العمود)، واللون، والشكل، والحجم. وهذه الفكرة هي قواعد بناء المخطط (chart grammar): المخطط مجموعة من عمليات الربط، وليس قالبًا له اسم. والقرّاء يقدّرون بعض هذه الخصائص بدقة أكبر من غيرها:
- الموضع على مقياس مشترك (الأدق)؛
- الطول (أعمدة تبدأ من خط أساس مشترك)؛
- الزاوية والمساحة (المخططات الدائرية والفقاعات)؛
- شدة اللون ودرجته (الأقل دقة في قراءة الكميات؛ وجيد للفئات).
لذلك تأخذ المقارنة الأهم الموضع أو الطول؛ واللون للـتجميع، لا لقراءة القيم.
الجزء B — من السؤال إلى المخطط: دليل
| نوع السؤال | مثال على السؤال | المخطط الافتراضي |
|---|---|---|
| كمية (مقارنة قيم عبر فئات) | "Which region had the most revenue?" (أي: "أي منطقة حققت أعلى إيرادات؟") | مخطط أعمدة مرتّب (أفقي إذا كانت التسميات طويلة) |
| توزيع (كيف تنتشر القيم) | "How long do deliveries take?" (أي: "كم يستغرق التوصيل؟") | مدرّج تكراري أو مخطط صندوقي (L52) |
| نسبة (أجزاء من كل) | "What share of revenue is Fresh?" (أي: "ما حصة فئة Fresh من الإيرادات؟") | أعمدة مكدّسة أو أعمدة بسيطة للحصص؛ والمخطط الدائري فقط لجزأين أو ثلاثة |
| علاقة (متغيران رقميان) | "Do farther deliveries take longer?" (أي: "هل يستغرق التوصيل الأبعد وقتًا أطول؟") | مخطط انتشار (L56) |
| اتجاه زمني (التغير عبر الزمن) | "How did revenue develop this year?" (أي: "كيف تطورت الإيرادات هذا العام؟") | مخطط خطي والزمن على المحور x |
هذا دليل المخططات (chart directory) نقطة بداية؛ ويضيف L63 قواعد تصميم لكل عائلة من المخططات.
الجزء C — Figure وAxes: الواجهة الصريحة
Figure في Matplotlib هي الصورة كلها؛ وAxes هي مساحة رسم واحدة داخلها، لها محورا x وy الخاصان بها وعنوانها وتسمياتها. والواجهة الكائنية (object-oriented interface) تنشئ الاثنين صراحةً وتستدعي الدوال عليهما:
rev_by_region = perf.groupby("region")["revenue"].sum().sort_values()
fig, ax = plt.subplots(figsize=(7, 3.5))
bars = ax.barh(rev_by_region.index, rev_by_region.values / 1000)
ax.set_title("Online (WEB) brought the most revenue, January–September 2026")
ax.set_xlabel("Revenue (€ thousand)")
ax.set_ylabel("Region")
fig.tight_layout()
fig.savefig("figures/revenue_by_region.png", dpi=150)
print("bars:", len(bars), "| order (bottom to top):", list(rev_by_region.index))
print("x label:", ax.get_xlabel(), "| saved:", Path("figures/revenue_by_region.png").exists())
bars: 5 | order (bottom to top): ['STA', 'NOR', 'CEN', 'HAR', 'WEB']
x label: Revenue (€ thousand) | saved: True
(عنوان المخطط معناه: "القناة الإلكترونية (WEB) حققت أعلى إيرادات، يناير–سبتمبر 2026"؛ وتسمية المحور x: "الإيرادات (بآلاف اليوروهات)".)
fig, ax = plt.subplots(figsize=(width, height))يحدد الحجم بالبوصة؛ax.barhيربط المنطقة بالموضع على y والإيرادات بـالطول على x؛- العنوان يذكر النتيجة، والتسمية تذكر الوحدة؛
fig.tight_layout()يمنع قصّ التسميات؛ وfig.savefigيكتب الملف (dpi=150لصيغة PNG؛ وصيغة.svgتعطي ملفًا متجهيًا يتغير حجمه دون أن يتشوش).
لماذا الواجهة الصريحة؟ عندما يكون في الدفتر عدة مخططات، توضّح استدعاءات ax. بلا لبس أي رسم تغيّره، ويعمل الكود نفسه داخل الدوال.
الجزء D — .plot في pandas هو Matplotlib في الخلفية
DataFrame.plot وSeries.plot تستدعيان Matplotlib نيابةً عنك وتعيدان Axes. مرّر ax= لتحتفظ بالتحكم:
monthly = perf.groupby("month")["revenue"].sum() / 1000
fig, ax = plt.subplots(figsize=(7, 3))
returned = monthly.plot(ax=ax, marker="o")
ax.set_title("Revenue grew every month in 2026")
ax.set_xlabel("Month")
ax.set_ylabel("Revenue (€ thousand)")
print("pandas returned the same Axes:", returned is ax, "| points:", len(ax.lines[0].get_xdata()))
pandas returned the same Axes: True | points: 9
(العنوان معناه: "نمت الإيرادات كل شهر في 2026".)
استخدم رسم pandas للمخططات السريعة؛ وانتقل إلى استدعاءات ax. المباشرة عندما تحتاج إلى تحكم كامل (L63–L64).
الجزء E — دالة رسم قابلة لإعادة الاستخدام
المخططات التي ترسمها أكثر من مرة مكانها دالة تأخذ البيانات وAxes وتعيد الـAxes:
def bar_chart(ax, series, title, xlabel, scale=1.0):
"""Sorted horizontal bar chart of a Series (largest at the top)."""
s = (series / scale).sort_values()
ax.barh(s.index, s.values)
ax.set_title(title)
ax.set_xlabel(xlabel)
return ax
fig, ax = plt.subplots(figsize=(6, 3))
bar_chart(ax, perf.groupby("category")["gross_margin"].sum(), "Drinks and Pantry earn most gross margin", "Gross margin (€ thousand)", 1000)
fig.savefig("figures/margin_by_category.svg")
print("labels:", [t.get_text() for t in ax.get_yticklabels()])
labels: ['Household', 'Fresh', 'Pantry', 'Drinks']
(سطر التوثيق في الدالة معناه: "مخطط أعمدة أفقي مرتّب لسلسلة Series، والأكبر في الأعلى". والعنوان معناه: "فئتا Drinks وPantry تحققان أكبر هامش ربح إجمالي".)
الدالة تعيد بناء النمط نفسه في كل مرة، وتعمل من جديد بعد إعادة تشغيل النواة لأنها تعتمد على مدخلاتها فقط.
5. مثال محلول
Cedar Lane (خيالية)، بيانات مصطنعة. تسأل مديرة العمليات: "What share of our deliveries are late, by region?" (أي: "ما نسبة التوصيلات المتأخرة عندنا، حسب المنطقة؟")
الخطوة 1 — نوع السؤال. نسبة لكل فئة (حصة التأخير حسب المنطقة) هي في الحقيقة مقارنة كميات بين معدّلات، إذن: مخطط أعمدة مرتّب للنسب المئوية، يبدأ من الصفر.
الخطوة 2 — احسب الأرقام أولًا (يجب أن يطابق المخطط الجدول).
t = perf.groupby("region")[["late_deliveries", "deliveries"]].sum()
t["late_share"] = t["late_deliveries"] / t["deliveries"]
print(t.sort_values("late_share", ascending=False).round(4).to_string())
late_deliveries deliveries late_share
region
HAR 49 930 0.0527
NOR 33 748 0.0441
CEN 36 826 0.0436
STA 18 419 0.0430
WEB 113 2836 0.0398
الخطوة 3 — ابنِ، وافحص، واحفظ.
s = (t["late_share"] * 100).sort_values()
fig, ax = plt.subplots(figsize=(6.5, 3))
ax.barh(s.index, s.values)
ax.set_xlim(0, max(s.values) * 1.15)
ax.set_title("About 1 in 20 deliveries is late in every region")
ax.set_xlabel("Late deliveries (% of deliveries), Jan–Sep 2026")
fig.tight_layout()
fig.savefig("figures/late_share_by_region.png", dpi=150)
print("x starts at", ax.get_xlim()[0], "| values match table:", [round(v, 2) for v in s.values] == [round(v * 100, 2) for v in t["late_share"].sort_values().values])
x starts at 0.0 | values match table: True
(العنوان معناه: "تقريبًا توصيلة واحدة من كل 20 تتأخر في كل منطقة"؛ وتسمية المحور: "التوصيلات المتأخرة (% من التوصيلات)، يناير–سبتمبر 2026".)
الفروق بين المناطق صغيرة (حوالي نقطة مئوية واحدة بين الأعلى والأدنى)، والعنوان يقول ذلك. أما عنوان مثل "Late deliveries by region" (أي: "التوصيلات المتأخرة حسب المنطقة") فيترك القارئ يستنتج ذلك بنفسه، أو يتخيّل فرقًا غير موجود.
6. شرح بصري
QUESTION ──▶ TYPE ──▶ CHART ──▶ MAPPING ──▶ LABELS ──▶ CHECK ──▶ SAVE
"Which region amount sorted region→y position title = finding numbers = table PNG (dpi) / SVG
sold most?" bars revenue→bar length axis labels+units axis from 0 (bars)
ACCURACY OF ENCODINGS: position > length > angle/area > colour (colour = groups, not amounts)
Figure (whole image) ⊃ Axes (one plot: x axis, y axis, title) fig, ax = plt.subplots(); ax.barh(...); ax.set_title(...)
وصف الرسم: تدفق من اليسار إلى اليمين: من سؤال صاحب المصلحة إلى نوعه، ثم المخطط، ثم ربط البيانات بالخصائص البصرية، ثم التسميات، ثم الفحص، ثم الحفظ. ولسؤال "أي منطقة باعت أكثر؟": النوع كمية، والمخطط أعمدة مرتّبة، والمنطقة مربوطة بالموضع على y والإيرادات بطول العمود، والعنوان يذكر النتيجة، وتسميات المحاور فيها الوحدات، والأرقام مطابقة للجدول، والأعمدة تبدأ من الصفر، والحفظ بصيغة PNG أو SVG. وفي الأسفل ترتيب أنواع الترميز حسب دقة القراءة: الموضع، ثم الطول، ثم الزاوية والمساحة، ثم اللون، وهو للمجموعات لا للكميات. وFigure تحتوي Axes؛ والواجهة الصريحة تنشئ الاثنين وتستدعي الدوال على الـAxes. (نصوص الرسم: QUESTION = السؤال؛ TYPE = النوع؛ CHART = المخطط؛ MAPPING = الربط؛ LABELS = التسميات؛ CHECK = الفحص؛ SAVE = الحفظ؛ amount = كمية؛ sorted bars = أعمدة مرتّبة؛ title = finding = العنوان = النتيجة؛ numbers = table = الأرقام = الجدول؛ axis from 0 = المحور من الصفر؛ ACCURACY OF ENCODINGS = دقة أنواع الترميز؛ colour = groups, not amounts = اللون للمجموعات لا للكميات؛ whole image = الصورة كلها؛ one plot = رسم واحد.)
7. تدريب موجَّه
افتح L62_starter.ipynb. أعد بناء ثلاثة رسوم عملية على طريقة U14 لتصبح مخططات نظيفة باستخدام fig, ax، بعنوان يذكر النتيجة، وتسميات محاور فيها الوحدات، وملف محفوظ لكل منها:
- الإيرادات حسب المنطقة (أعمدة أفقية مرتّبة، بآلاف اليوروهات).
- الإيرادات الشهرية لكل المناطق معًا (مخطط خطي، والأشهر على x).
- حصة التأخير الشهرية لـ WEB مقابل كل المناطق الأخرى مجتمعة (خطّان؛ سمِّ كلًّا منهما).
- لكل مخطط، اطبع الأرقام التي يقف عليها وطابقها مع القيم المرسومة.
- شغّل Restart Kernel and Run All Cells… وتأكد من أن الملفات الثلاثة أُعيد إنشاؤها.
8. تدريب مستقل
خمسة أسئلة من أصحاب المصلحة. لكل سؤال، اذكر نوع السؤال، والمخطط الذي ستستخدمه، وسطرًا واحدًا يشرح السبب (الربط ودقة القراءة):
- "Which product category has the highest gross margin rate?" (أي: "أي فئة منتجات لها أعلى معدل هامش ربح إجمالي؟")
- "How did Online (WEB) orders develop month by month compared with the four stores?" (أي: "كيف تطورت الطلبات الإلكترونية (WEB) شهرًا بعد شهر مقارنةً بالمتاجر الأربعة؟")
- "What share of our revenue comes from each region?" (أي: "ما حصة كل منطقة من إيراداتنا؟")
- "Is there a relationship between a region's order volume and its late share?" (أي: "هل هناك علاقة بين حجم طلبات المنطقة وحصة التأخير فيها؟") (استخدم نقاطًا لكل منطقة × شهر)
- "How are monthly revenues per region and category spread?" (أي: "كيف تتوزع الإيرادات الشهرية لكل منطقة وفئة؟")
ثم ابنِ اثنين منها (من اختيارك) كمخططات نهائية: عنوان يذكر النتيجة، وتسميات محاور فيها الوحدات، وملفات محفوظة، وفحص مطبوع يثبت أن الأرقام المرسومة تطابق الجدول.
اعمل في L62_starter.ipynb (الجزء 2).
9. اختبار الفهم (Knowledge Check) — قواعد بناء المخطط
Q1. أي خاصية جمالية يقدّرها القرّاء بأعلى دقة؟
- A) الموضع على مقياس مشترك
- B) المساحة
- C) درجة اللون
- D) الزاوية
Q2. "How did revenue develop this year?" (كيف تطورت الإيرادات هذا العام؟) أي مخطط تختار؟
- A) مخطط دائري
- B) مخطط خطي والزمن على المحور x
- C) مخطط انتشار للإيرادات مقابل المنطقة
- D) مدرّج تكراري
Q3. "Which region had the most revenue?" (أي منطقة حققت أعلى إيرادات؟) أي مخطط تختار؟
- A) مخطط صندوقي
- B) مخطط دائري بخمس شرائح
- C) مخطط خطي
- D) مخطط أعمدة مرتّب
Q4. ما الـAxes في Matplotlib؟
- A) الصورة كلها
- B) مساحة رسم واحدة لها محاورها وعنوانها وتسمياتها
- C) ملف محفوظ
- D) خريطة ألوان
Q5. ماذا يعيد revenue_series.plot(ax=ax)؟
- A) الـAxes التي رسم عليها
- B) DataFrame
- C) لا شيء
- D) Figure جديدة
Q6. أي عنوان هو الأفضل لمخطط حصة التأخير حسب المنطقة؟
- A) "Chart 3" (أي: "المخطط 3")
- B) "Data" (أي: "بيانات")
- C) "About 1 in 20 deliveries is late in every region" (أي: "تقريبًا توصيلة من كل 20 تتأخر في كل منطقة")
- D) "Late deliveries by region" (أي: "التوصيلات المتأخرة حسب المنطقة")
Q7. متى تكون صيغة SVG أفضل من PNG؟
- A) أبدًا
- B) لقيمة dpi أصغر
- C) للصور الفوتوغرافية
- D) عندما يجب أن يتغير حجم المخطط دون أن يتشوش (رسوم متجهية)
Q8. ماذا يجب أن يرمّز اللون في معظم مخططات الأعمال؟
- A) المجموعات أو الفئات (والتأكيد)، بينما توضع الكميات على الموضع أو الطول
- B) الكميات الدقيقة
- C) لا شيء
- D) العنوان
10. الذكاء الاصطناعي في التطبيق (اختياري، وفي حدود)
الهدف: أن تطلب من مساعد ذكاء اصطناعي اقتراح مخطط لسؤال ما؛ ولا تقبله إلا إذا طابق نوع السؤال في الدليل وطابق بياناتك.
- اسأل:
"What chart should I use to show what share of revenue each of five regions brings?"
وبالعربية: "أي مخطط أستخدم لأعرض حصة كل منطقة من خمس مناطق من الإيرادات؟"
- قارن إجابته بالجزء B.
الحدود:
- استخدام الذكاء الاصطناعي اختياري.
- شارك السؤال وأسماء الأعمدة فقط.
- التوصية التي تتجاهل عدد الفئات أو دقة القراءة لا تُقبل كما هي.
11. التحقق من مخرجات الذكاء الاصطناعي
- النوع: هل تعرّف على أنه سؤال نسبة؟
- الملاءمة: هل يناسب مخططه خمس فئات (المخطط الدائري بخمس شرائح متقاربة صعب القراءة)؟
- البيانات: هل لديك الأعمدة التي يفترضها؟
- دوّن اختيارك النهائي وسببه.
12. منظور الأعمال
المخطط أداة لاتخاذ القرار. المديرون يقضون ثواني أمام المخطط. والعنوان الذي يذكر النتيجة مع نوع المخطط المناسب يعنيان أنهم يقرؤون الإجابة، لا المخطط. ودوال الرسم القابلة لإعادة الاستخدام تجعل التقارير الشهرية متّسقة أيضًا، فيتعلم القرّاء الشكل مرة واحدة.
13. مهمة عمل — مخطط ملف مجلس الإدارة
نشاط خيالي: Cedar Lane. يحتاج مسؤول المالية إلى مخطط واحد لملف مجلس الإدارة (board pack): هامش الربح الإجمالي حسب الفئة من يناير إلى سبتمبر، بعنوان يذكر النتيجة.
- ابنِه (اختر النوع وبرّر اختيارك)، واحفظه بصيغة SVG، واطبع الأرقام التي يقف عليها.
- اكتب تعليقًا توضيحيًا من جملة واحدة يذكر المصدر والفترة.
14. تحدٍّ قصير — المخطط الافتراضي
نشاط خيالي: Cedar Lane. مخطط رسمه زميل:
fig, ax = plt.subplots()
perf.groupby("region")["revenue"].sum().plot(kind="pie", ax=ax)
print([t.get_text() for t in ax.texts][:5])
- قبل تشغيله، اكتب ما لا يستطيع القارئ رؤيته بسهولة في هذا المخطط.
- شغّله ثم أعد بناءه كمخطط يجيب عن "which region brings the most revenue, and by how much?" (أي: "أي منطقة تجلب أعلى إيرادات، وبفارق كم؟").
- أضف عنوانًا يذكر النتيجة، ووحدة.
- اكتب جملة واحدة يحتفظ بها زميلك كقاعدة.
سلّم عملك قبل أن تتلقى الملاحظات. لا يوجد حل منشور هنا.
15. المصادر
مطلوب (حوالي 40 دقيقة):
- Claus O. Wilke: Fundamentals of Data Visualization، قسم "Directory of visualizations" والفصل الخاص بربط البيانات بالخصائص الجمالية. https://clauswilke.com/dataviz/directory-of-visualizations.html (فُتح في 20 أكتوبر 2026.)
- Matplotlib: Quick start guide، قسما "Parts of a Figure" و"Coding styles" (الواجهة الصريحة مقابل الضمنية). https://matplotlib.org/stable/users/explain/quick_start.html
موصى به:
- pandas: Chart visualization، عن
plotوax=. https://pandas.pydata.org/docs/user_guide/visualization.html
16. شخصية تتابعها / قدوة
Claus O. Wilke (اختارته D.World للدروس L62–L64).
- حقيقة مُتحقَّق منها: مؤلف الكتاب الإلكتروني Fundamentals of Data Visualization (قدوات المرحلة 1 في D.World، R04، "بتعمّق أكبر في المرحلة 3").
- لماذا توصي به D.World: الكتاب يشرح اختيار المخططات من خلال الخصائص الجمالية ودقة القراءة، مع أمثلة جيدة وسيئة جنبًا إلى جنب.
- ملاحظة صادقة: لا نذكر إلا الحقيقة المذكورة أعلاه.
- جرّب هذا: في دليل المخططات (directory of visualizations)، ابحث عن القسم الخاص بالنسب. متى يقترح المخطط الدائري؟
17. المصطلحات
| المصطلح | معناه في هذا الدرس |
|---|---|
| الربط الجمالي (Aesthetic mapping) | إسناد متغير من البيانات إلى خاصية بصرية (الموضع، أو الطول، أو اللون، أو الشكل) |
| قواعد بناء المخطط (Chart grammar) | النظر إلى المخطط كمجموعة من عمليات الربط، لا كقالب له اسم |
| Figure | صورة Matplotlib كلها |
| Axes | مساحة رسم واحدة داخل Figure، لها محاورها وعنوانها وتسمياتها |
| الواجهة الكائنية (Object-oriented interface) | إنشاء fig, ax صراحةً واستدعاء الدوال عليهما |
| دليل المخططات (Chart directory) | جدول يربط نوع السؤال بمخطط افتراضي |
للمراجعة: أول الرسوم (L52) · مناظر التحليل الاستكشافي العملية (U14).
18. تأمّل
- أي من رسومك في U14 سيربك مديرًا، ولماذا؟
- متى ستخرج عن الاختيار الافتراضي في الدليل؟
- ماذا ستذكر عناوين كل مخططاتك من الآن فصاعدًا؟
19. الخطوة التالية
التالي: L63 — إتقان مخططات المقارنة والاتجاهات والتوزيعات والعلاقات (Comparisons, Trends, Distributions & Relationships Done Well). قواعد تصميم لكل عائلة من المخططات، بما فيها عرض عدم اليقين.
20. أدلة التقييم
| الدليل | ماذا يُظهر | كيف يُقيَّم |
|---|---|---|
| خمسة اختيارات للمخططات + مخططان مبنيّان (التدريب المستقل) | أنواع الأسئلة والمخططات مبرّرة؛ ومخططات نهائية بعناوين ووحدات وملفات محفوظة وفحوص للأرقام | دليل التقييم L62-IP |
| التدريب الموجَّه (ثلاثة مخططات أُعيد بناؤها) | واجهة fig/ax؛ والتسميات والوحدات؛ والفحوص؛ وإعادة التشغيل | دليل التقييم L62-GP |
| اختبار الفهم (Q1–Q8) | أنواع الترميز، والدليل، وFigure/Axes، والعناوين، والصيغ | 7 إجابات صحيحة أو أكثر من 8 |
| مهمة العمل والتحدي القصير | مخطط مجلس الإدارة مع تعليقه؛ والمخطط الدائري وقد أُعيد بناؤه أعمدةً | دليلا التقييم L62-WT وL62-MC |
المراجع
- Wilke, C. O. Fundamentals of Data Visualization. https://clauswilke.com/dataviz/directory-of-visualizations.html (WILKE). يدعم: الخصائص الجمالية، والدليل؛ وحقيقة القدوة.
- Matplotlib developers. Quick start guide. https://matplotlib.org/stable/users/explain/quick_start.html (MPL-QS). يدعم: Figure/Axes، والواجهة الصريحة، والحفظ.
- pandas development team. Chart visualization. https://pandas.pydata.org/docs/user_guide/visualization.html (P-VIS). يدعم:
.plotكغلاف. - D.World Stage 3 Detailed Lesson Map v0.9 (صف L62). وثيقة داخلية.
L63 · Comparisons, Trends, Distributions & Relationships Done Well Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
L64 · Honest & Accessible Charts Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
L65 · Data Storytelling for Decisions Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
U15 Exit = Project 5 — Visualization & Storytelling Membersللمشتركين
The exit brief is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
تكليف نهاية الوحدة ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
U16 · Data-Driven Business & Decision Makingاتخاذ القرار المبني على البيانات
Stage 3المرحلة 3Free: the first lesson. The other lessons, the exit brief and the practice files are for Starter and Membership.
مجاني: الدرس الأول. باقي الدروس وتكليف نهاية الوحدة وملفات التمرين لأصحاب باقة البداية والاشتراك.
Download U16 practice files (.zip, 428 KB)حمّل ملفات تمرين U16 (zip، 428 KB)
Unzip into a short folder such as D:\DWorld on Windows. The zip has only learner files: no answers.
فك الضغط في مجلد قصير مثل D:\DWorld على ويندوز. الملف يحتوي ملفات المتعلم فقط بدون إجابات.
L66 · Analytics for Decisions: Framing, KPI Trees & the Cost of Being WrongL66 · التحليلات من أجل القرارات: صياغة القرار، وأشجار مؤشرات الأداء، وتكلفة الخطأ Freeمجاني
L66 — Analytics for Decisions: Framing, KPI Trees & the Cost of Being Wrong
Learner file. Answers and model solutions are in a separate instructor-only file. Version: U16 Production v2 · Content date: 20 October 2026. Time estimates are planning estimates until learners have tested them. The code in this lesson was run with the course setup from L21 (Python 3.13, pandas 3.0 and NumPy 2.5; SciPy 1.18 and Matplotlib 3.11 where used) on Linux and on Windows 11 on 4 November 2026 (see the Code Test Report).
All businesses are fictional and all data is synthetic.
1. Lesson Identity
| Field | Value |
|---|---|
| Title | Analytics for Decisions: Framing, KPI Trees & the Cost of Being Wrong |
| Stage | Stage 3 — Data Analysis |
| Unit | U16 — Data-Driven Business Decisions |
| Lesson ID | L66 |
| Skills | Descriptive, diagnostic, predictive, prescriptive questions; decision framing (options, metric, guardrails, threshold); KPI trees; leading and lagging indicators; false positives and negatives; expected cost of a decision rule |
| Difficulty | Intermediate |
| Estimated time | About 2.5 hours (planning estimate, unvalidated) |
| Assessment level | Application |
| Prerequisites | U02 (KPIs), L55 (Type I/II errors), L61 (driver decomposition), L65 (audience and decision). |
Time breakdown (planning estimate): concept and worked example 55 min · guided practice 30 min · independent practice 35 min · knowledge check 10 min · AI 5 min (optional) · workplace task and mini challenge 15 min.
What you need: practice_files (L66_starter.ipynb; u16_data/fraud_review_counts.csv, u16_data/cedar_lane_u16.db).
The U16 data. cedar_lane_u16.db is a SQLite database with customers (acquisition channel and month), orders (one row per order, with revenue and gross margin, by month) and marketing_spend (monthly spend and platform-attributed revenue per channel), January–September 2026. Other U16 files are introduced in the lessons that use them.
2. Learning Objective
By the end of this lesson, you will be able to:
- classify an analytical question as descriptive, diagnostic, predictive or prescriptive;
- frame a decision with options, a metric, guardrails and a threshold, using a KPI tree;
- compare two decision rules by the expected cost of their errors.
D.World approved objective (verbatim): Classify analytical questions as descriptive, diagnostic, predictive or prescriptive, frame a decision with options, a metric, guardrails and a threshold, and compare two decision rules by expected cost of errors.
3. Why It Matters
"We need a churn model." "Build me a dashboard." Requests often name a tool, not a decision. Analysts who build the tool as asked often deliver something nobody uses, because nobody decided what would happen differently depending on the answer.
Decision framing turns a request into: what are the options? which metric decides between them? what must not get worse? at what value would we choose differently? And because every decision rule makes mistakes, the best rule is often not the most "accurate" one but the one whose mistakes cost least.
Key principle: start from the decision, not the analysis: name the options, the deciding metric, the guardrails and the threshold, and compare rules by what their errors cost.
4. Concept
import sqlite3
import numpy as np
import pandas as pd
fraud = pd.read_csv("u16_data/fraud_review_counts.csv")
con = sqlite3.connect("u16_data/cedar_lane_u16.db")
q = lambda sql: pd.read_sql_query(sql, con)
print(fraud.to_string(index=False))
threshold orders_flagged fraud_caught fraud_missed reviews_wasted
0.5 900 46 4 854
0.7 380 41 9 339
0.9 120 30 20 90
Part A — Four kinds of analytical question
| Kind | Asks | Example at Cedar Lane | Typical tools |
|---|---|---|---|
| Descriptive | What happened? | "How many customers did each channel bring this year?" | counts, KPIs, charts (U07–U15) |
| Diagnostic | Why did it happen? | "Why did North's repeat rate fall?" | EDA, decomposition, tests (U13–U14) |
| Predictive | What will happen? | "How many orders will North have next week?" | forecasts (L70), models (Stage 4) |
| Prescriptive | What should we do? | "How should we split next quarter's marketing budget?" | scenarios, optimisation (L71) |
Many requests mix kinds. Naming the kind tells you what evidence is needed: a prescriptive question needs options and a decision rule, not just a chart.
Part B — Framing a decision
A decision frame has five parts:
- Decision and owner: what will be decided, by whom, by when.
- Options: at least two, including "do nothing".
- Deciding metric: the one number that chooses between options (for example gross margin per month).
- Guardrail metrics: what must not get worse (for example delivery late rate, refund rate).
- Threshold: the value of the deciding metric at which you would switch options, set before looking at results (L55).
Add: what evidence would change the decision. If no possible result would change it, the analysis is not needed.
Part C — KPI trees
A KPI tree breaks a top metric into the drivers that multiply or add up to it (L61's decomposition, drawn as a tree):
Gross margin per month
├── Orders per month
│ ├── Active customers
│ │ ├── New customers (acquisition)
│ │ └── Retained customers (1 − churn)
│ └── Orders per active customer
└── Gross margin per order
├── Average order value
└── Margin rate (price, cost, discounts, delivery cost)
Check that the arithmetic works with real numbers:
m = q("""
SELECT order_month AS month, COUNT(*) AS orders, COUNT(DISTINCT customer_id) AS active_customers,
ROUND(SUM(gross_margin), 2) AS gross_margin
FROM orders GROUP BY order_month ORDER BY order_month
""")
m["orders_per_active"] = m["orders"] / m["active_customers"]
m["margin_per_order"] = m["gross_margin"] / m["orders"]
m["rebuilt"] = m["active_customers"] * m["orders_per_active"] * m["margin_per_order"]
print(m.round(2).tail(3).to_string(index=False))
print("tree multiplies back to the top metric:", bool(np.allclose(m["rebuilt"], m["gross_margin"])))
month orders active_customers gross_margin orders_per_active margin_per_order rebuilt
2026-07 750 634 5929.05 1.18 7.91 5929.05
2026-08 820 678 6521.56 1.21 7.95 6521.56
2026-09 838 700 6662.03 1.20 7.95 6662.03
tree multiplies back to the top metric: True
A leading indicator moves before the outcome (new customers this month, sessions with items in the basket); a lagging indicator reports the outcome after the fact (monthly gross margin). Decisions often monitor leading indicators because they give earlier warning.
Part D — False positives, false negatives and expected cost
Any rule that sorts cases (review this order or not; contact this customer or not) makes two kinds of mistake:
- a false positive: the rule acts, but there was nothing to act on (a review of a genuine order);
- a false negative: the rule does not act, but it should have (a fraudulent order not reviewed).
They rarely cost the same. The expected cost of a rule adds up the cost of each kind of error times how often it happens:
expected cost = (false negatives × cost of a miss) + (false positives × cost of a wasted action)
COST_MISSED = 120.0 # average loss per undetected fraudulent order (given by finance)
COST_REVIEW = 4.0 # staff cost of one unnecessary manual review (given by operations)
fraud["expected_cost"] = fraud["fraud_missed"] * COST_MISSED + fraud["reviews_wasted"] * COST_REVIEW
print(fraud[["threshold", "fraud_missed", "reviews_wasted", "expected_cost"]].to_string(index=False))
print("lowest expected cost at threshold:", fraud.loc[fraud["expected_cost"].idxmin(), "threshold"])
threshold fraud_missed reviews_wasted expected_cost
0.5 4 854 3896.0
0.7 9 339 2436.0
0.9 20 90 2760.0
lowest expected cost at threshold: 0.7
The rule that catches the most fraud (threshold 0.5) is not the cheapest: it creates 854 wasted reviews. The rule with the fewest reviews (0.9) misses too much fraud. The middle threshold has the lowest expected cost under these cost assumptions. If the costs change, the best rule can change, so state the costs as assumptions and show how the choice moves:
for cm in [60, 120, 240]:
cost = fraud["fraud_missed"] * cm + fraud["reviews_wasted"] * COST_REVIEW
print(f"cost of a miss €{cm:>3}: best threshold {fraud.loc[cost.idxmin(), 'threshold']}, costs {cost.round(0).tolist()}")
cost of a miss € 60: best threshold 0.9, costs [3656.0, 1896.0, 1560.0]
cost of a miss €120: best threshold 0.7, costs [3896.0, 2436.0, 2760.0]
cost of a miss €240: best threshold 0.7, costs [4376.0, 3516.0, 5160.0]
5. Worked Example
Cedar Lane (fictional), synthetic data. The marketing lead says: "We need a churn model."
Step 1 — Kind of question. The request names a predictive tool. The real question is prescriptive: should we spend on keeping customers, and on which ones?
Step 2 — Decision frame.
- Decision and owner: whether to send a retention offer (a €5 voucher) to customers at risk of not ordering next month; marketing lead; decided monthly.
- Options: (a) no offer; (b) offer to everyone active this month; (c) offer only to customers flagged as at risk.
- Deciding metric: gross margin over the next two months minus voucher cost.
- Guardrails: voucher redemption by customers who would have ordered anyway; margin rate.
- Threshold: send offers only if the extra margin retained per voucher is above its €5 cost.
Step 3 — What the data can say now. How many active customers do not order the next month?
act = q("SELECT DISTINCT customer_id, order_month FROM orders")
months = sorted(act["order_month"].unique())
lost = []
for a, b in zip(months[:-1], months[1:]):
cur, nxt = set(act.loc[act.order_month == a, "customer_id"]), set(act.loc[act.order_month == b, "customer_id"])
lost.append(len(cur - nxt) / len(cur))
print("share of active customers with no order next month:", [round(x, 3) for x in lost])
print("average:", round(float(np.mean(lost)), 3))
share of active customers with no order next month: [0.249, 0.231, 0.269, 0.252, 0.284, 0.253, 0.229, 0.273]
average: 0.255
Step 4 — What would change the decision. If about a quarter of active customers lapse each month, option (b) gives many vouchers to people who would have ordered anyway (false positives). A churn model is only worth building if it can flag lapsing customers well enough that the margin saved exceeds voucher cost plus wasted vouchers; that needs a small test (L69), not just a model. Frame for the marketing lead: "Before building a model, test the voucher on a random half of at-risk customers for one month and measure retained margin against cost."
6. Visual Explanation
REQUEST ("we need a churn model") ──▶ KIND (predictive tool, prescriptive question) ──▶ FRAME
decision · owner · options (incl. do nothing)
deciding metric · guardrails · threshold · what would change it
KPI TREE: margin = customers × orders/customer × margin/order leading (new customers) → lagging (monthly margin)
RULE MISTAKES acts does not act EXPECTED COST = FN × cost(miss) + FP × cost(waste)
truly needed ✔ true positive ✘ false negative threshold 0.5: few misses, many wasted reviews
not needed ✘ false positive ✔ true negative threshold 0.9: few reviews, many misses → choose by cost
Alt text: A request flows to its question kind and to a decision frame with the decision, owner, options including doing nothing, the deciding metric, guardrails, threshold and what would change it. A KPI tree shows margin as customers times orders per customer times margin per order, with leading and lagging indicators. A two-by-two grid of rule outcomes shows true and false positives and negatives, and the expected cost formula: false negatives times the cost of a miss plus false positives times the cost of waste, with a low threshold giving few misses but many wasted reviews and a high threshold the opposite.
7. Guided Practice
Open L66_starter.ipynb.
- Reframe "we need a churn model" in your own words: kind of question, decision, owner, options, deciding metric, guardrails, threshold, what would change the decision.
- Build the KPI tree for monthly gross margin from the
orderstable and check that it multiplies back to the top metric for every month. - Fraud review thresholds: compute the expected cost of each threshold with a miss costing €120 and a wasted review €4; then with a miss costing €60 and €240. Which threshold is best in each case?
- Write one sentence on how the choice depends on the cost assumptions.
- Run Restart Kernel and Run All Cells….
8. Independent Practice
A new pickup point. The operations director asks whether Cedar Lane should open a sixth pickup point next year. Write a one-page decision frame (250–400 words) that includes:
- the kind of question and the decision (owner, deadline, options including "do nothing");
- a KPI tree for the deciding metric (draw it as text), with at least one leading indicator;
- guardrail metrics;
- a threshold (in numbers, with how you would set it), and the costs of deciding wrongly in each direction;
- what evidence would change the decision, and how you would collect it (data you have, data you would need, a pilot).
Use the database for any baseline numbers you quote (for example current gross margin per order).
Work in L66_starter.ipynb (Part 2).
9. Knowledge Check — Decisions
Q1. "How should we split next quarter's marketing budget?" is which kind of question?
- A) Prescriptive
- B) Predictive
- C) Diagnostic
- D) Descriptive
Q2. Which belongs in every decision frame?
- A) Ten charts
- B) Options (including "do nothing"), a deciding metric, guardrails and a threshold
- C) A machine-learning model
- D) A dashboard
Q3. What is a guardrail metric?
- A) A forecast
- B) The main metric
- C) A metric that must not get worse while optimising the main one
- D) A chart axis
Q4. Which is a leading indicator of monthly gross margin?
- A) Annual profit
- B) New customers acquired this month
- C) Last year's revenue
- D) Last month's gross margin
Q5. A review rule flags a genuine order. What is that?
- A) False negative
- B) True positive
- C) True negative
- D) False positive
Q6. How is the expected cost of a rule computed here?
- A) Number of flags only
- B) Accuracy × 100
- C) False negatives × cost of a miss + false positives × cost of a wasted action
- D) True positives − false positives
Q7. When the cost of missing fraud doubles, what tends to happen to the best threshold?
- A) It stays the same always
- B) It moves to flag more orders (lower threshold)
- C) Thresholds do not depend on costs
- D) It moves to flag fewer orders
Q8. When is an analysis not needed for a decision?
- A) When the data are small
- B) When the manager is busy
- C) Never
- D) When no possible result would change the decision
10. AI in Practice (optional, bounded)
Goal: ask an AI assistant to turn a vague request into a decision frame, then check it against Part B.
- Ask: "Our CEO wants a dashboard of customer churn. Turn this into a decision frame with options, metric, guardrails and a threshold."
- Check each part against Part B; mark missing or vague parts.
Boundaries: AI is optional; share the request, not data; a frame without a numeric threshold or without "do nothing" is incomplete.
11. AI Verification
- Options: does it include "do nothing"?
- Threshold: is it numeric and set in advance?
- Guardrails: are they specific to Cedar Lane's business (delivery, margin, refunds)?
- Write down two improvements you made.
12. Business Lens
Analytics creates value only when it changes a decision. Framing first prevents months of work on models nobody uses, and the expected-cost view replaces arguments about "accuracy" with the question that matters to finance: which rule loses least money? Stating costs as assumptions also invites the right people (finance, operations) to correct them.
13. Workplace Task — The Threshold Memo
Fictional business: Cedar Lane. Operations asks which fraud-review threshold to use. Write a memo (80–120 words) recommending a threshold, showing expected costs under the given assumptions, and saying which cost estimate the recommendation is most sensitive to.
14. Mini Challenge — The Most Accurate Rule
Fictional business: Cedar Lane. A colleague picks threshold 0.9 because "it has the fewest wrong flags":
print(fraud[["threshold", "orders_flagged", "reviews_wasted", "fraud_missed"]].to_string(index=False))
- Before computing anything, say what the colleague's criterion ignores.
- Compute expected costs with the given assumptions and compare.
- Under what cost of a missed fraud would 0.9 be the best choice? (Try values.)
- Write one sentence your colleague can keep as a rule.
Submit before feedback. No solution is provided here.
15. Resources
Required (about 30 min):
- Cassie Kozyrkov: home page, writing on decision intelligence (deciding before analysing). https://www.kozyr.com/ (Opened 20 Oct 2026.)
Recommended:
- AWS Cloud Adoption Framework — Business Perspective: Data science, on framing data-science work around business outcomes. https://docs.aws.amazon.com/whitepapers/latest/aws-caf-business-perspective/data-science.html
- AWS Cloud Adoption Framework — Business Perspective: Business insights, on linking analytics to decisions. https://docs.aws.amazon.com/whitepapers/latest/aws-caf-business-perspective/business-insights.html
16. Person to Follow / Role Model
Cassie Kozyrkov (mapped by D.World for L66, L67, L70, L71).
- Verified fact: she describes herself as a "Decision Intelligence Pioneer" and "Google's First Chief Decision Scientist" (D.World Stage 1 role-model record R01).
- Why D.World recommends her: her work starts from the decision and its costs, the frame of this lesson.
- Honest note: we quote only her own description.
- Try this: find one of her pieces on setting a decision threshold before looking at data. Summarise it in one sentence.
17. Glossary
| Term | Meaning in this lesson |
|---|---|
| Descriptive analytics | What happened? |
| Diagnostic analytics | Why did it happen? |
| Predictive analytics | What will happen? |
| Prescriptive analytics | What should we do? |
| KPI tree | A top metric broken into the drivers that combine to produce it |
| Decision threshold | The value of the deciding metric at which you switch options, set in advance |
| Guardrail metric | A metric that must not get worse while optimising the main one |
| Leading indicator | A metric that moves before the outcome |
| Lagging indicator | A metric that reports the outcome after the fact |
| False positive | The rule acts when there was nothing to act on |
| False negative | The rule does not act when it should have |
| Expected cost | The sum of each error type's frequency times its cost |
Review: KPIs (U02) · Type I/II errors (L55) · decomposition (L61).
18. Reflection
- Which recent request in your own work named a tool rather than a decision?
- Whose cost estimates would you need to trust an expected-cost comparison?
- What is a guardrail metric in your own work?
19. Next Step
Next: L67 — Customer & Marketing Economics. Churn, CAC, LTV, payback and ROAS by acquisition channel, with their assumptions.
20. Assessment Evidence
| Evidence | What it shows | How it is judged |
|---|---|---|
| Pickup-point decision frame (Independent Practice) | Kind; options incl. do nothing; KPI tree with leading indicator; guardrails; numeric threshold; costs of both errors; evidence plan | Rubric L66-IP |
| Guided practice (reframe; KPI tree; fraud thresholds) | Frame complete; tree reconciles; expected costs under three assumptions | Rubric L66-GP |
| Knowledge check (Q1–Q8) | Question kinds, frames, guardrails, indicators, errors, expected cost | 7 or more of 8 correct |
| Workplace task and mini challenge | Threshold memo with sensitivity; accuracy vs cost | Rubrics L66-WT, L66-MC |
References
- Kozyrkov, C. Home page. https://www.kozyr.com/ (KOZYR). Supports: decision intelligence; role-model description (R01).
- Amazon Web Services. AWS CAF — Business Perspective: Business insights. https://docs.aws.amazon.com/whitepapers/latest/aws-caf-business-perspective/business-insights.html (R06).
- D.World Stage 3 Detailed Lesson Map v0.9 (L66 row). Internal.
L66 — التحليلات من أجل القرارات: صياغة القرار، وأشجار مؤشرات الأداء، وتكلفة الخطأ
ملف المتعلّم. الإجابات والحلول النموذجية في ملف منفصل خاص بالمدرّس فقط. الإصدار: U16 Production v2 · تاريخ المحتوى: 20 أكتوبر 2026. تقديرات الوقت تقديرات للتخطيط إلى أن يختبرها المتعلّمون. الكود في هذا الدرس شُغّل باستخدام تجهيزات الدورة من L21 (Python 3.13 وpandas 3.0 وNumPy 2.5؛ وSciPy 1.18 وMatplotlib 3.11 حيث استُخدمتا) على Linux وعلى Windows 11 في 4 نوفمبر 2026 (راجع تقرير اختبار الكود Code Test Report).
كل الأنشطة التجارية خيالية، وكل البيانات مصطنعة.
1. بطاقة الدرس
| البند | القيمة |
|---|---|
| العنوان | التحليلات من أجل القرارات: صياغة القرار، وأشجار مؤشرات الأداء، وتكلفة الخطأ |
| المرحلة | المرحلة 3 — تحليل البيانات |
| الوحدة | U16 — قرارات الأعمال المبنية على البيانات |
| رقم الدرس | L66 |
| المهارات | الأسئلة الوصفية والتشخيصية والتنبؤية والتوجيهية؛ صياغة القرار (الخيارات، والمقياس، ومقاييس الحماية، والعتبة)؛ أشجار مؤشرات الأداء (KPI trees)؛ المؤشرات القائدة والمتأخرة؛ الإيجابيات الكاذبة والسلبيات الكاذبة؛ التكلفة المتوقعة لقاعدة القرار |
| المستوى | متوسط |
| الوقت المتوقع | حوالي ساعتين ونصف (تقدير للتخطيط، غير مُتحقَّق منه) |
| مستوى التقييم | تطبيق |
| المتطلبات السابقة | U02 (مؤشرات الأداء الرئيسية)، وL55 (أخطاء النوع الأول والثاني)، وL61 (تفكيك المحرّكات)، وL65 (الجمهور والقرار). |
تفصيل الوقت (تقدير للتخطيط):
- المفهوم والمثال المحلول: 55 دقيقة.
- التدريب الموجَّه: 30 دقيقة.
- التدريب المستقل: 35 دقيقة.
- اختبار الفهم: 10 دقائق.
- الذكاء الاصطناعي: 5 دقائق (اختياري).
- مهمة العمل والتحدي القصير: 15 دقيقة.
ما تحتاج إليه: مجلد practice_files (L66_starter.ipynb؛ وu16_data/fraud_review_counts.csv وu16_data/cedar_lane_u16.db).
بيانات U16. cedar_lane_u16.db قاعدة بيانات SQLite فيها customers (قناة الاستقطاب وشهره)، وorders (صف لكل طلب، مع الإيرادات وهامش الربح الإجمالي، حسب الشهر)، وmarketing_spend (الإنفاق الشهري والإيرادات التي تنسبها المنصّة لكل قناة)، من يناير إلى سبتمبر 2026. أما ملفات U16 الأخرى فتُقدَّم في الدروس التي تستخدمها.
2. هدف التعلّم
في نهاية هذا الدرس ستكون قادرًا على أن:
- تصنّف السؤال التحليلي: وصفي، أو تشخيصي، أو تنبؤي، أو توجيهي؛
- تصوغ قرارًا بخيارات ومقياس ومقاييس حماية وعتبة، مستخدمًا شجرة مؤشرات أداء؛
- تقارن بين قاعدتَي قرار بالتكلفة المتوقعة لأخطائهما.
الهدف المعتمد من D.World (بنصّه): Classify analytical questions as descriptive, diagnostic, predictive or prescriptive, frame a decision with options, a metric, guardrails and a threshold, and compare two decision rules by expected cost of errors.
3. لماذا يهمّك هذا؟
"We need a churn model." (أي: "نحتاج إلى نموذج لتسرّب العملاء.") "Build me a dashboard." (أي: "ابنِ لي لوحة متابعة.") كثيرًا ما تسمّي الطلبات أداة، لا قرارًا. والمحلّلون الذين يبنون الأداة كما طُلبت كثيرًا ما يسلّمون شيئًا لا يستخدمه أحد، لأن أحدًا لم يحدد ما الذي سيتغير بحسب الإجابة.
وصياغة القرار (decision framing) تحوّل الطلب إلى: ما الخيارات؟ وأي مقياس يحسم بينها؟ وما الذي يجب ألّا يسوء؟ وعند أي قيمة سنختار خيارًا مختلفًا؟ ولأن كل قاعدة قرار تخطئ، فإن أفضل قاعدة كثيرًا ما لا تكون "الأدق"، بل التي تكلّف أخطاؤها أقل.
المبدأ الأساسي: ابدأ من القرار لا من التحليل: سمِّ الخيارات، والمقياس الحاسم، ومقاييس الحماية، والعتبة، وقارن القواعد بما تكلّفه أخطاؤها.
4. المفهوم
import sqlite3
import numpy as np
import pandas as pd
fraud = pd.read_csv("u16_data/fraud_review_counts.csv")
con = sqlite3.connect("u16_data/cedar_lane_u16.db")
q = lambda sql: pd.read_sql_query(sql, con)
print(fraud.to_string(index=False))
threshold orders_flagged fraud_caught fraud_missed reviews_wasted
0.5 900 46 4 854
0.7 380 41 9 339
0.9 120 30 20 90
الجزء A — أربعة أنواع من الأسئلة التحليلية
| النوع | يسأل | مثال في Cedar Lane | الأدوات المعتادة |
|---|---|---|---|
| وصفي (Descriptive) | ماذا حدث؟ | "How many customers did each channel bring this year?" (أي: "كم عميلًا جلبت كل قناة هذا العام؟") | العدّ، ومؤشرات الأداء، والمخططات (U07–U15) |
| تشخيصي (Diagnostic) | لماذا حدث؟ | "Why did North's repeat rate fall?" (أي: "لماذا انخفض معدل الشراء المتكرر في North؟") | التحليل الاستكشافي، والتفكيك، والاختبارات (U13–U14) |
| تنبؤي (Predictive) | ماذا سيحدث؟ | "How many orders will North have next week?" (أي: "كم طلبًا سيكون لدى North الأسبوع القادم؟") | التنبؤات (L70)، والنماذج (المرحلة 4) |
| توجيهي (Prescriptive) | ماذا يجب أن نفعل؟ | "How should we split next quarter's marketing budget?" (أي: "كيف نوزّع ميزانية التسويق للربع القادم؟") | السيناريوهات، والتحسين الأمثل (L71) |
كثير من الطلبات تخلط بين الأنواع. وتسمية النوع تخبرك بالأدلة المطلوبة: فالسؤال التوجيهي يحتاج إلى خيارات وقاعدة قرار، لا إلى مخطط فقط.
الجزء B — صياغة القرار
لـإطار القرار (decision frame) خمسة أجزاء:
- القرار وصاحبه: ما الذي سيُقرَّر، ومن يقرّره، ومتى.
- الخيارات: اثنان على الأقل، منها "عدم فعل شيء".
- المقياس الحاسم (deciding metric): الرقم الواحد الذي يختار بين الخيارات (مثل هامش الربح الإجمالي في الشهر).
- مقاييس الحماية (guardrail metrics): ما يجب ألّا يسوء (مثل معدل تأخير التوصيل، ومعدل الاسترداد).
- العتبة (threshold): قيمة المقياس الحاسم التي ستغيّر عندها الخيار، وتُحدَّد قبل النظر إلى النتائج (L55).
وأضف: ما الأدلة التي ستغيّر القرار. فإذا لم تكن هناك نتيجة ممكنة تغيّره، فلا حاجة إلى التحليل.
الجزء C — أشجار مؤشرات الأداء
شجرة مؤشرات الأداء (KPI tree) تفكّك المقياس الأعلى إلى المحرّكات التي تُضرب أو تُجمع لتنتجه (تفكيك L61 مرسومًا على هيئة شجرة):
Gross margin per month
├── Orders per month
│ ├── Active customers
│ │ ├── New customers (acquisition)
│ │ └── Retained customers (1 − churn)
│ └── Orders per active customer
└── Gross margin per order
├── Average order value
└── Margin rate (price, cost, discounts, delivery cost)
(ترجمة الشجرة: هامش الربح الإجمالي في الشهر ← الطلبات في الشهر [العملاء النشطون (عملاء جدد بالاستقطاب، وعملاء محتفَظ بهم = 1 − معدل التسرّب)، والطلبات لكل عميل نشط]، وهامش الربح الإجمالي لكل طلب [متوسط قيمة الطلب، ومعدل الهامش (السعر، والتكلفة، والخصومات، وتكلفة التوصيل)].)
تحقّق من أن الحساب يصح بأرقام حقيقية:
m = q("""
SELECT order_month AS month, COUNT(*) AS orders, COUNT(DISTINCT customer_id) AS active_customers,
ROUND(SUM(gross_margin), 2) AS gross_margin
FROM orders GROUP BY order_month ORDER BY order_month
""")
m["orders_per_active"] = m["orders"] / m["active_customers"]
m["margin_per_order"] = m["gross_margin"] / m["orders"]
m["rebuilt"] = m["active_customers"] * m["orders_per_active"] * m["margin_per_order"]
print(m.round(2).tail(3).to_string(index=False))
print("tree multiplies back to the top metric:", bool(np.allclose(m["rebuilt"], m["gross_margin"])))
month orders active_customers gross_margin orders_per_active margin_per_order rebuilt
2026-07 750 634 5929.05 1.18 7.91 5929.05
2026-08 820 678 6521.56 1.21 7.95 6521.56
2026-09 838 700 6662.03 1.20 7.95 6662.03
tree multiplies back to the top metric: True
المؤشر القائد (leading indicator) يتحرك قبل النتيجة (العملاء الجدد هذا الشهر، والجلسات التي فيها منتجات في السلة)؛ والمؤشر المتأخر (lagging indicator) يرصد النتيجة بعد وقوعها (هامش الربح الإجمالي الشهري). وكثيرًا ما تراقب القرارات المؤشرات القائدة لأنها تنبّه مبكرًا.
الجزء D — الإيجابيات الكاذبة، والسلبيات الكاذبة، والتكلفة المتوقعة
أي قاعدة تفرز الحالات (نراجع هذا الطلب أم لا؛ نتواصل مع هذا العميل أم لا) ترتكب نوعين من الأخطاء:
- إيجابي كاذب (false positive): القاعدة تتصرف، لكن لم يكن هناك ما يستدعي التصرف (مراجعة طلب سليم)؛
- سلبي كاذب (false negative): القاعدة لا تتصرف، مع أنه كان يجب أن تتصرف (طلب احتيالي لم يُراجَع).
ونادرًا ما تتساوى تكلفتهما. والتكلفة المتوقعة (expected cost) لقاعدة ما تجمع تكلفة كل نوع من الأخطاء مضروبة في عدد مرات حدوثه:
التكلفة المتوقعة = (السلبيات الكاذبة × تكلفة الإخفاق في الاكتشاف) + (الإيجابيات الكاذبة × تكلفة الإجراء المهدَر)
COST_MISSED = 120.0 # average loss per undetected fraudulent order (given by finance)
COST_REVIEW = 4.0 # staff cost of one unnecessary manual review (given by operations)
fraud["expected_cost"] = fraud["fraud_missed"] * COST_MISSED + fraud["reviews_wasted"] * COST_REVIEW
print(fraud[["threshold", "fraud_missed", "reviews_wasted", "expected_cost"]].to_string(index=False))
print("lowest expected cost at threshold:", fraud.loc[fraud["expected_cost"].idxmin(), "threshold"])
threshold fraud_missed reviews_wasted expected_cost
0.5 4 854 3896.0
0.7 9 339 2436.0
0.9 20 90 2760.0
lowest expected cost at threshold: 0.7
(التعليقان في الكود معناهما: متوسط الخسارة لكل طلب احتيالي لم يُكتشف (من قسم المالية)؛ وتكلفة الموظفين لمراجعة يدوية واحدة غير ضرورية (من قسم العمليات).)
القاعدة التي تكتشف أكبر قدر من الاحتيال (العتبة 0.5) ليست الأرخص: فهي تُنتج 854 مراجعة مهدرة. والقاعدة ذات أقل عدد من المراجعات (0.9) يفوتها احتيال كثير. أما العتبة الوسطى فلها أقل تكلفة متوقعة في ظل افتراضات التكلفة هذه. وإذا تغيرت التكاليف قد تتغير القاعدة الأفضل، لذلك اذكر التكاليف بوصفها افتراضات، وبيّن كيف يتحرك الاختيار:
for cm in [60, 120, 240]:
cost = fraud["fraud_missed"] * cm + fraud["reviews_wasted"] * COST_REVIEW
print(f"cost of a miss €{cm:>3}: best threshold {fraud.loc[cost.idxmin(), 'threshold']}, costs {cost.round(0).tolist()}")
cost of a miss € 60: best threshold 0.9, costs [3656.0, 1896.0, 1560.0]
cost of a miss €120: best threshold 0.7, costs [3896.0, 2436.0, 2760.0]
cost of a miss €240: best threshold 0.7, costs [4376.0, 3516.0, 5160.0]
5. مثال محلول
Cedar Lane (خيالية)، بيانات مصطنعة. يقول مسؤول التسويق: "We need a churn model." (أي: "نحتاج إلى نموذج لتسرّب العملاء.")
الخطوة 1 — نوع السؤال. الطلب يسمّي أداة تنبؤية. أما السؤال الحقيقي فـتوجيهي: هل ننفق على الاحتفاظ بالعملاء، وعلى أيّهم؟
الخطوة 2 — إطار القرار.
- القرار وصاحبه: هل نرسل عرض احتفاظ (قسيمة بقيمة 5 يورو) للعملاء المعرّضين لعدم الطلب في الشهر القادم؛ وصاحبه مسؤول التسويق؛ ويُتخذ شهريًا.
- الخيارات: (a) لا عرض؛ (b) عرض لكل من كان نشطًا هذا الشهر؛ (c) عرض فقط للعملاء المصنَّفين معرّضين للخطر.
- المقياس الحاسم: هامش الربح الإجمالي خلال الشهرين القادمين مطروحًا منه تكلفة القسائم.
- مقاييس الحماية: استخدام القسائم من عملاء كانوا سيطلبون على أي حال؛ ومعدل الهامش.
- العتبة: لا ترسل العروض إلا إذا كان الهامش الإضافي المحتفَظ به لكل قسيمة أعلى من تكلفتها البالغة 5 يورو.
الخطوة 3 — ما الذي تستطيع البيانات قوله الآن. كم عميلًا نشطًا لا يطلب في الشهر التالي؟
act = q("SELECT DISTINCT customer_id, order_month FROM orders")
months = sorted(act["order_month"].unique())
lost = []
for a, b in zip(months[:-1], months[1:]):
cur, nxt = set(act.loc[act.order_month == a, "customer_id"]), set(act.loc[act.order_month == b, "customer_id"])
lost.append(len(cur - nxt) / len(cur))
print("share of active customers with no order next month:", [round(x, 3) for x in lost])
print("average:", round(float(np.mean(lost)), 3))
share of active customers with no order next month: [0.249, 0.231, 0.269, 0.252, 0.284, 0.253, 0.229, 0.273]
average: 0.255
الخطوة 4 — ما الذي سيغيّر القرار. إذا كان حوالي ربع العملاء النشطين يتوقفون كل شهر، فالخيار (b) يعطي قسائم كثيرة لأشخاص كانوا سيطلبون على أي حال (إيجابيات كاذبة). ونموذج التسرّب لا يستحق البناء إلا إذا استطاع تحديد العملاء المتوقفين بدرجة تجعل الهامش الذي يُنقَذ أكبر من تكلفة القسائم مضافًا إليها القسائم المهدرة؛ وهذا يحتاج إلى اختبار صغير (L69)، لا إلى نموذج فقط. الصياغة المقترحة لمسؤول التسويق: "Before building a model, test the voucher on a random half of at-risk customers for one month and measure retained margin against cost." (أي: "قبل بناء نموذج، جرّب القسيمة على نصف عشوائي من العملاء المعرّضين للخطر لمدة شهر، وقِس الهامش المحتفَظ به مقابل التكلفة.")
6. شرح بصري
REQUEST ("we need a churn model") ──▶ KIND (predictive tool, prescriptive question) ──▶ FRAME
decision · owner · options (incl. do nothing)
deciding metric · guardrails · threshold · what would change it
KPI TREE: margin = customers × orders/customer × margin/order leading (new customers) → lagging (monthly margin)
RULE MISTAKES acts does not act EXPECTED COST = FN × cost(miss) + FP × cost(waste)
truly needed ✔ true positive ✘ false negative threshold 0.5: few misses, many wasted reviews
not needed ✘ false positive ✔ true negative threshold 0.9: few reviews, many misses → choose by cost
وصف الرسم: طلب يتدفق إلى نوع سؤاله ثم إلى إطار قرار فيه القرار، وصاحبه، والخيارات ومنها عدم فعل شيء، والمقياس الحاسم، ومقاييس الحماية، والعتبة، وما الذي سيغيّره. وشجرة مؤشرات أداء تبيّن الهامش على أنه العملاء × الطلبات لكل عميل × الهامش لكل طلب، مع مؤشرات قائدة ومتأخرة. وشبكة اثنين في اثنين لنتائج القاعدة تبيّن الإيجابيات والسلبيات الصحيحة والكاذبة، ومعادلة التكلفة المتوقعة: السلبيات الكاذبة × تكلفة الإخفاق + الإيجابيات الكاذبة × تكلفة الهدر؛ فالعتبة المنخفضة تعطي إخفاقات قليلة لكن مراجعات مهدرة كثيرة، والعتبة المرتفعة العكس. (نصوص الرسم: REQUEST = الطلب؛ KIND = النوع؛ predictive tool, prescriptive question = أداة تنبؤية، وسؤال توجيهي؛ FRAME = الإطار؛ decision · owner · options (incl. do nothing) = القرار · صاحبه · الخيارات (ومنها عدم فعل شيء)؛ deciding metric · guardrails · threshold · what would change it = المقياس الحاسم · مقاييس الحماية · العتبة · ما الذي سيغيّره؛ leading → lagging = قائد ← متأخر؛ RULE MISTAKES = أخطاء القاعدة؛ acts = تتصرف؛ does not act = لا تتصرف؛ truly needed = مطلوب فعلًا؛ not needed = غير مطلوب؛ true positive = إيجابي صحيح؛ false negative = سلبي كاذب؛ false positive = إيجابي كاذب؛ true negative = سلبي صحيح؛ EXPECTED COST = التكلفة المتوقعة؛ few misses, many wasted reviews = إخفاقات قليلة ومراجعات مهدرة كثيرة؛ few reviews, many misses → choose by cost = مراجعات قليلة وإخفاقات كثيرة ← اختر حسب التكلفة.)
7. تدريب موجَّه
افتح L66_starter.ipynb.
- أعد صياغة "we need a churn model" بكلماتك: نوع السؤال، والقرار، وصاحبه، والخيارات، والمقياس الحاسم، ومقاييس الحماية، والعتبة، وما الذي سيغيّر القرار.
- ابنِ شجرة مؤشرات الأداء لهامش الربح الإجمالي الشهري من جدول
orders، وتحقق من أن حاصل ضربها يعود إلى المقياس الأعلى في كل شهر. - عتبات مراجعة الاحتيال: احسب التكلفة المتوقعة لكل عتبة إذا كانت تكلفة الإخفاق 120 يورو وتكلفة المراجعة المهدرة 4 يورو؛ ثم إذا كانت تكلفة الإخفاق 60 يورو و240 يورو. أي عتبة هي الأفضل في كل حالة؟
- اكتب جملة واحدة عن اعتماد الاختيار على افتراضات التكلفة.
- شغّل Restart Kernel and Run All Cells….
8. تدريب مستقل
نقطة استلام جديدة. يسأل مدير العمليات هل يجب أن تفتح Cedar Lane نقطة استلام سادسة العام القادم. اكتب إطار قرار في صفحة واحدة (من 250 إلى 400 كلمة) يتضمن:
- نوع السؤال والقرار (صاحبه، والموعد النهائي، والخيارات ومنها "عدم فعل شيء")؛
- شجرة مؤشرات أداء للمقياس الحاسم (ارسمها نصًّا)، فيها مؤشر قائد واحد على الأقل؛
- مقاييس الحماية؛
- عتبة (بالأرقام، مع طريقة تحديدها)، وتكاليف القرار الخاطئ في كل اتجاه؛
- ما الأدلة التي ستغيّر القرار، وكيف ستجمعها (بيانات لديك، وبيانات ستحتاج إليها، وتجربة تجريبية pilot).
استخدم قاعدة البيانات لأي أرقام أساسية تذكرها (مثل هامش الربح الإجمالي الحالي لكل طلب).
اعمل في L66_starter.ipynb (الجزء 2).
9. اختبار الفهم (Knowledge Check) — القرارات
Q1. "How should we split next quarter's marketing budget?" (كيف نوزّع ميزانية التسويق للربع القادم؟) من أي نوع هذا السؤال؟
- A) توجيهي
- B) تنبؤي
- C) تشخيصي
- D) وصفي
Q2. ما الذي يجب أن يكون في كل إطار قرار؟
- A) عشرة مخططات
- B) الخيارات (ومنها "عدم فعل شيء")، ومقياس حاسم، ومقاييس حماية، وعتبة
- C) نموذج تعلّم آلي
- D) لوحة متابعة
Q3. ما مقياس الحماية؟
- A) تنبؤ
- B) المقياس الرئيسي
- C) مقياس يجب ألّا يسوء أثناء تحسين المقياس الرئيسي
- D) محور في مخطط
Q4. أي مما يلي مؤشر قائد لهامش الربح الإجمالي الشهري؟
- A) الربح السنوي
- B) العملاء الجدد الذين استُقطبوا هذا الشهر
- C) إيرادات العام الماضي
- D) هامش الربح الإجمالي في الشهر الماضي
Q5. قاعدة مراجعة علّمت طلبًا سليمًا. ماذا يُسمّى ذلك؟
- A) سلبي كاذب
- B) إيجابي صحيح
- C) سلبي صحيح
- D) إيجابي كاذب
Q6. كيف تُحسب التكلفة المتوقعة لقاعدة في هذا الدرس؟
- A) بعدد الحالات المعلَّمة فقط
- B) الدقة × 100
- C) السلبيات الكاذبة × تكلفة الإخفاق + الإيجابيات الكاذبة × تكلفة الإجراء المهدَر
- D) الإيجابيات الصحيحة − الإيجابيات الكاذبة
Q7. عندما تتضاعف تكلفة عدم اكتشاف الاحتيال، ماذا يحدث غالبًا للعتبة الأفضل؟
- A) تبقى كما هي دائمًا
- B) تتحرك لتعلّم طلبات أكثر (عتبة أقل)
- C) العتبات لا تعتمد على التكاليف
- D) تتحرك لتعلّم طلبات أقل
Q8. متى لا يكون التحليل ضروريًا لقرار ما؟
- A) عندما تكون البيانات قليلة
- B) عندما يكون المدير مشغولًا
- C) أبدًا
- D) عندما لا توجد نتيجة ممكنة تغيّر القرار
10. الذكاء الاصطناعي في التطبيق (اختياري، وفي حدود)
الهدف: أن تطلب من مساعد ذكاء اصطناعي تحويل طلب غامض إلى إطار قرار، ثم تراجعه مقابل الجزء B.
- اسأل:
"Our CEO wants a dashboard of customer churn. Turn this into a decision frame with options, metric, guardrails and a threshold."
وبالعربية: "الرئيس التنفيذي يريد لوحة متابعة لتسرّب العملاء. حوّل هذا إلى إطار قرار بخيارات ومقياس ومقاييس حماية وعتبة."
- راجع كل جزء مقابل الجزء B؛ وعلّم الأجزاء الناقصة أو الغامضة.
الحدود:
- استخدام الذكاء الاصطناعي اختياري.
- شارك الطلب، لا البيانات.
- الإطار الذي ليس فيه عتبة رقمية أو ليس فيه "عدم فعل شيء" ناقص.
11. التحقق من مخرجات الذكاء الاصطناعي
- الخيارات: هل يتضمن "عدم فعل شيء"؟
- العتبة: هل هي رقمية ومحددة مسبقًا؟
- مقاييس الحماية: هل هي خاصة بنشاط Cedar Lane (التوصيل، والهامش، والاستردادات)؟
- دوّن تحسينين أجريتهما.
12. منظور الأعمال
التحليلات لا تصنع قيمة إلا عندما تغيّر قرارًا. فالصياغة أولًا تمنع شهورًا من العمل على نماذج لا يستخدمها أحد، ومنظور التكلفة المتوقعة يستبدل الجدل حول "الدقة" بالسؤال الذي يهم قسم المالية: أي قاعدة تخسر أقل مال؟ وذكر التكاليف بوصفها افتراضات يدعو أيضًا الأشخاص المناسبين (المالية، والعمليات) إلى تصحيحها.
13. مهمة عمل — مذكرة العتبة
نشاط خيالي: Cedar Lane. يسأل قسم العمليات أي عتبة لمراجعة الاحتيال يستخدم. اكتب مذكرة (من 80 إلى 120 كلمة) توصي بعتبة، وتعرض التكاليف المتوقعة في ظل الافتراضات المعطاة، وتذكر أي تقدير للتكلفة هي التوصية أكثر حساسية له.
14. تحدٍّ قصير — القاعدة الأدق
نشاط خيالي: Cedar Lane. يختار زميل العتبة 0.9 لأن "it has the fewest wrong flags" (أي: "فيها أقل عدد من التعليمات الخاطئة"):
print(fraud[["threshold", "orders_flagged", "reviews_wasted", "fraud_missed"]].to_string(index=False))
- قبل أن تحسب أي شيء، اذكر ما الذي يتجاهله معيار زميلك.
- احسب التكاليف المتوقعة بالافتراضات المعطاة وقارن.
- عند أي تكلفة لعدم اكتشاف الاحتيال تصبح 0.9 الاختيار الأفضل؟ (جرّب قيمًا.)
- اكتب جملة واحدة يحتفظ بها زميلك كقاعدة.
سلّم عملك قبل أن تتلقى الملاحظات. لا يوجد حل منشور هنا.
15. المصادر
مطلوب (حوالي 30 دقيقة):
- Cassie Kozyrkov: home page، كتاباتها عن ذكاء القرار (decision intelligence)، أي أن تقرر قبل أن تحلل. https://www.kozyr.com/ (فُتح في 20 أكتوبر 2026.)
موصى به:
- AWS Cloud Adoption Framework — Business Perspective: Data science، عن بناء عمل علم البيانات حول نتائج الأعمال. https://docs.aws.amazon.com/whitepapers/latest/aws-caf-business-perspective/data-science.html
- AWS Cloud Adoption Framework — Business Perspective: Business insights، عن ربط التحليلات بالقرارات. https://docs.aws.amazon.com/whitepapers/latest/aws-caf-business-perspective/business-insights.html
16. شخصية تتابعها / قدوة
Cassie Kozyrkov (اختارتها D.World للدروس L66 وL67 وL70 وL71).
- حقيقة مُتحقَّق منها: تصف نفسها بأنها "Decision Intelligence Pioneer" (رائدة في ذكاء القرار) و"Google's First Chief Decision Scientist" (أول كبيرة علماء القرار في Google) (سجل القدوات في المرحلة 1 من D.World، R01).
- لماذا توصي بها D.World: عملها يبدأ من القرار وتكاليفه، وهو إطار هذا الدرس.
- ملاحظة صادقة: لا نقتبس إلا وصفها لنفسها.
- جرّب هذا: ابحث عن مقال لها عن تحديد عتبة القرار قبل النظر إلى البيانات. لخّصه في جملة واحدة.
17. المصطلحات
| المصطلح | معناه في هذا الدرس |
|---|---|
| التحليلات الوصفية (Descriptive analytics) | ماذا حدث؟ |
| التحليلات التشخيصية (Diagnostic analytics) | لماذا حدث؟ |
| التحليلات التنبؤية (Predictive analytics) | ماذا سيحدث؟ |
| التحليلات التوجيهية (Prescriptive analytics) | ماذا يجب أن نفعل؟ |
| شجرة مؤشرات الأداء (KPI tree) | مقياس أعلى مفكَّك إلى المحرّكات التي تجتمع لتنتجه |
| عتبة القرار (Decision threshold) | قيمة المقياس الحاسم التي تغيّر عندها الخيار، وتُحدَّد مسبقًا |
| مقياس الحماية (Guardrail metric) | مقياس يجب ألّا يسوء أثناء تحسين المقياس الرئيسي |
| المؤشر القائد (Leading indicator) | مقياس يتحرك قبل النتيجة |
| المؤشر المتأخر (Lagging indicator) | مقياس يرصد النتيجة بعد وقوعها |
| إيجابي كاذب (False positive) | القاعدة تتصرف حين لم يكن هناك ما يستدعي التصرف |
| سلبي كاذب (False negative) | القاعدة لا تتصرف حين كان يجب أن تتصرف |
| التكلفة المتوقعة (Expected cost) | مجموع تكرار كل نوع من الأخطاء مضروبًا في تكلفته |
للمراجعة: مؤشرات الأداء الرئيسية (U02) · أخطاء النوع الأول والثاني (L55) · التفكيك (L61).
18. تأمّل
- أي طلب حديث في عملك سمّى أداة بدلًا من قرار؟
- تقديرات تكلفة من ستحتاج إلى الثقة بها لتعتمد على مقارنة بالتكلفة المتوقعة؟
- ما مقياس الحماية في عملك؟
19. الخطوة التالية
التالي: L67 — اقتصاديات العملاء والتسويق (Customer & Marketing Economics). معدل التسرّب، وتكلفة استقطاب العميل (CAC)، والقيمة الدائمة للعميل (LTV)، وفترة الاسترداد، والعائد على الإنفاق الإعلاني (ROAS) حسب قناة الاستقطاب، مع افتراضاتها.
20. أدلة التقييم
| الدليل | ماذا يُظهر | كيف يُقيَّم |
|---|---|---|
| إطار قرار نقطة الاستلام (التدريب المستقل) | النوع؛ والخيارات ومنها عدم فعل شيء؛ وشجرة مؤشرات أداء فيها مؤشر قائد؛ ومقاييس الحماية؛ وعتبة رقمية؛ وتكاليف الخطأين؛ وخطة الأدلة | دليل التقييم L66-IP |
| التدريب الموجَّه (إعادة الصياغة؛ وشجرة المؤشرات؛ وعتبات الاحتيال) | إطار مكتمل؛ وشجرة تتطابق أرقامها؛ وتكاليف متوقعة في ظل ثلاثة افتراضات | دليل التقييم L66-GP |
| اختبار الفهم (Q1–Q8) | أنواع الأسئلة، والأطر، ومقاييس الحماية، والمؤشرات، والأخطاء، والتكلفة المتوقعة | 7 إجابات صحيحة أو أكثر من 8 |
| مهمة العمل والتحدي القصير | مذكرة العتبة مع الحساسية؛ والدقة مقابل التكلفة | دليلا التقييم L66-WT وL66-MC |
المراجع
- Kozyrkov, C. Home page. https://www.kozyr.com/ (KOZYR). يدعم: ذكاء القرار؛ ووصف القدوة (R01).
- Amazon Web Services. AWS CAF — Business Perspective: Business insights. https://docs.aws.amazon.com/whitepapers/latest/aws-caf-business-perspective/business-insights.html (R06).
- D.World Stage 3 Detailed Lesson Map v0.9 (صف L66). وثيقة داخلية.
L67 · Customer & Marketing Economics: Churn, CAC, LTV, Payback & ROAS Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
L68 · Product Analytics: Funnels, Activation & Feature Adoption Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
L69 · Experiments for Business Decisions: Designing & Reading A/B Tests Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
L70 · Forecasting with Baselines: Naive, Seasonal Naive & Moving Average Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
L71 · Prescriptive Thinking: Pricing Scenarios & Budget Allocation Membersللمشتركين
This lesson is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
هذا الدرس ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
U16 Exit = Project 4 — Customer / Marketing Analytics + Stage 3 Integrated Check Membersللمشتركين
The exit brief is part of Starter and Membership. Starter gives you 7 days of full access for EGP 269. Membership is EGP 999 for 30 days.
تكليف نهاية الوحدة ضمن باقة البداية والاشتراك. باقة البداية تمنحك 7 أيام وصول كامل مقابل 269 جنيهًا، والاشتراك 999 جنيهًا لمدة 30 يومًا.
Keep goingاستمر
Finish each unit's exit check before you move on. Your certificate lists the checks you passed.
أنهِ تقييم نهاية كل وحدة قبل أن تنتقل. شهادتك تعرض التقييمات التي اجتزتها.