سرنخ — خودآموز برنامه‌نویسی برای هوش مصنوعی (مقدماتی)

فصل ۱۰ از ۱۰

پیشرفت ترم
۰٪

ترم ۲ · داده: دیدن، تمیز کردن، فهمیدن

پروژه: یک دیتاستِ واقعی، از فایلِ خام تا گزارش

فصل ۱۰پیش‌نمایش رایگان
۱۰ دقیقه مطالعه فصل ۱۰

در این فصل چه یاد می‌گیری#

هیچ مفهومِ تازه‌ای در این فصل نیست. کاری که می‌کنی این است: کلِ ترمِ ۲ را در یک مسیرِ پیوسته می‌چینی — از فایلِ خام تا سه یافته که هر کدام شواهدِ خودشان را دارند. ولی موضوعِ اصلی این فصل کد نیست، گزارش است: یاد می‌گیری یافته را طوری بنویسی که خواننده بتواند به آن اعتماد کند، و همان‌قدر مهم، طوری که بداند کجا نباید اعتماد کند.

یک میزِ کارِ برش‌خورده که در یک سرش فایلِ خام و در سرِ دیگرش یک برگهٔ گزارشِ سه‌بخشی است

آخر این فصل می‌توانی:

  • یک زنجیرهٔ پاک‌سازیِ کامل بنویسی که کارش را گزارش می‌کند
  • سه یافته با شواهدِ عددی استخراج کنی
  • برای هر یافته بگویی چه چیزی را ثابت نمی‌کند
  • یک گزارشِ کوتاه بنویسی که کسِ دیگری بتواند بازتولیدش کند

قبل از شروع#

از فصل‌های ۳ تا ۹: خواندن، پاک‌سازی، گروه‌بندی، نمودار، تحلیلِ اکتشافی.

از ترمِ ۱ فصل ۷: تابع با return و رد کردنِ حالتِ خاص در خطِ اول.

📓 نوت‌بوک: نوت‌بوک این فصل را در Colab باز کن — همهٔ کدهای این فصل آماده و به‌ترتیب داخلش هست.

۱. زنجیره‌ای که کارش را گزارش می‌کند#

import pandas as pd

def load_and_clean(path):
    """فایلِ خام را می‌خواند، پاک‌سازی می‌کند، و (جدولِ تمیز، گزارشِ پاک‌سازی) را برمی‌گرداند."""
    raw = pd.read_csv(path)
    report = {"rows_in": len(raw)}

    df = raw.drop_duplicates().reset_index(drop=True)
    report["duplicates_removed"] = report["rows_in"] - len(df)

    df["price"] = df["price"].str.replace(",", "", regex=False).astype(int)
    df["brand"] = df["brand"].str.replace("ي", "ی").str.replace("ك", "ک").str.strip()
    df["city"] = df["city"].str.strip()
    report["brands_after_merge"] = df["brand"].nunique()

    report["ram_missing"] = int(df["ram_gb"].isna().sum())

    before = len(df)
    df = df[(df["year"] >= 2010) & (df["year"] <= 2025)].reset_index(drop=True)
    report["impossible_years_removed"] = before - len(df)
    report["rows_out"] = len(df)
    return df, report

df, report = load_and_clean("laptops.csv")
for key in report:
    print(f"{key}: {report[key]}")
rows_in: 244
duplicates_removed: 4
brands_after_merge: 5
ram_missing: 26
impossible_years_removed: 2
rows_out: 238

این تابع دو چیز برمی‌گرداند و هر دو لازم‌اند: جدولِ تمیز و گزارشِ اینکه چه بلایی سرش آمد.

بدونِ آن گزارش، کسی که نتیجه‌ات را می‌خواند نمی‌داند از ۲۴۴ ردیفِ اصلی چند تا واقعاً استفاده شده و چرا. با آن، در شش خط همه‌چیز روشن است.

چک کن: rows_in منهای حذف‌ها باید دقیقاً rows_out بدهد: ۲۴۴ − ۴ − ۲ = ۲۳۸. اگر نخواند، یکی از قدم‌ها ردیفی حذف کرده که گزارش نشده — و آن، بی‌سروصداترین اشتباهِ ممکن در پاک‌سازی است.

۲. یافتهٔ اول: قیمت با سال بالا می‌رود#

no_apple = df[df["brand"] != "اپل"]
yearly = no_apple.groupby("year")["price"].mean()

print("همبستگی سال و قیمت (کل):", df[["year", "price"]].corr().round(3).iloc[0, 1])
print("همبستگی سال و قیمت (بدون اپل):", no_apple[["year", "price"]].corr().round(3).iloc[0, 1])
print("نسبت آخرین سال به اولین سال:", round(yearly.iloc[-1] / yearly.iloc[0], 2))
همبستگی سال و قیمت (کل): 0.635
همبستگی سال و قیمت (بدون اپل): 0.895
نسبت آخرین سال به اولین سال: 2.35

یافته: بینِ سالِ ساخت و قیمتِ آگهی رابطهٔ قویِ صعودی هست. برای لپ‌تاپ‌های غیراپل، میانگینِ قیمت از قدیمی‌ترین تا تازه‌ترین سال ۲٫۳۵ برابر می‌شود و همبستگی ۰٫۸۹۵ است.

چه چیزی را ثابت نمی‌کند: اینکه لپ‌تاپ‌ها «گران‌تر شده‌اند». این داده عکسِ لحظه‌ایِ آگهی‌هاست، نه یک سریِ زمانیِ قیمت. لپ‌تاپِ تازه‌تر گران‌تر است — که کاملاً بدیهی است — ولی دربارهٔ تورم یا تغییرِ بازار هیچ نمی‌گوید.

۳. یافتهٔ دوم: اپل یک بازارِ جداست#

groups = df.groupby(df["brand"] == "اپل")["price"].agg(["count", "mean", "median"]).round(0)
print(groups)

q1, q3 = df["price"].quantile([0.25, 0.75])
high = q3 + 1.5 * (q3 - q1)
outliers = df[df["price"] > high]
print("پرت‌ها:", len(outliers), "| همه اپل؟", (outliers["brand"] == "اپل").all())
       count        mean      median
brand
False    210  23850000.0  23300000.0
True      28  46025000.0  45050000.0
پرت‌ها: 9 | همه اپل؟ True

آن .all() در خطِ آخر تازه است و ساده: همان ماسکِ True/False فصلِ ۲ را می‌گیرد و می‌پرسد «آیا همه‌شان درست‌اند؟». خواهرش .any() می‌پرسد «آیا دستِ‌کم یکی درست است؟». این دو، یک ماسکِ بلند را به یک جوابِ بله-خیر تبدیل می‌کنند — و در ترمِ ۵ که برای داده دروازهٔ اعتبارسنجی بسازیم، ستونِ فقراتِ آن دروازه‌اند.

یافته: اپل میانگینِ قیمتی حدودِ ۱٫۹ برابرِ بقیه دارد، و هر نُه مقدارِ پرتِ آماری مالِ همین برند است. این یک زیرگروهِ واقعی است، نه خطای داده.

چه چیزی را ثابت نمی‌کند: اینکه اپل «گران‌فروشی» می‌کند. ما مشخصاتِ کامل نداریم — فقط سال و حافظه. تفاوتِ قیمت می‌تواند کاملاً از تفاوتِ سخت‌افزار بیاید.

پیامدِ عملی: هر تحلیلی که اپل را با بقیه یکجا ببیند، هر دو گروه را بد توصیف می‌کند. یا جدا تحلیلشان کن، یا برند را صریح وارد مدل کن.

۴. یافتهٔ سوم: شهر تقریباً بی‌اثر است#

by_city = df.groupby("city")["price"].agg(["count", "mean"]).round(0)
print(by_city.sort_values("count", ascending=False))

city_means = df.groupby("city")["price"].mean()
print("نسبت گران‌ترین به ارزان‌ترین شهر:", round(city_means.max() / city_means.min(), 3))
print("پراکندگی نسبی میان شهرها:", round(city_means.std() / city_means.mean(), 3))
        count        mean
city
تهران      97  26337113.0
مشهد       45  23764444.0
شیراز      37  27567568.0
اصفهان     36  28025000.0
تبریز      23  28008696.0
نسبت گران‌ترین به ارزان‌ترین شهر: 1.179
پراکندگی نسبی میان شهرها: 0.067

یافته: تفاوتِ میانگینِ قیمت بینِ شهرها کوچک است — گران‌ترین شهر فقط ۱۸ درصد از ارزان‌ترین بالاتر است، و پراکندگیِ نسبیِ میانگین‌ها حدودِ ۷ درصد. در برابرِ تفاوتِ ۹۰ درصدیِ اپل با بقیه، این تقریباً هیچ است.

یافتهٔ منفی هم یافته است. «شهر تقریباً بی‌اثر است» یک نتیجهٔ کاملاً معتبر و مفید است: به تو می‌گوید وقتت را روی تحلیلِ منطقه‌ای تلف نکن.

📏 اندازه بگیر: ولی به count نگاه کن: تبریز فقط ۲۳ آگهی دارد. میانگینی که روی ۲۳ نمونه حساب شده، بی‌ثبات است. پس نتیجهٔ درست این نیست که «شهرها تفاوت ندارند»؛ این است که «با این حجمِ داده، تفاوتی که بشود به آن تکیه کرد دیده نمی‌شود». این دو جمله شبیه‌اند و یکی نیستند — و تفاوتشان همان چیزی است که یک تحلیل‌گرِ درست را از یک تحلیل‌گرِ پرمدعا جدا می‌کند.

۵. یافته‌ای که فقط با نگاه‌کردن پیدا می‌شود#

by_ram = df.groupby("ram_gb")["price"].agg(["count", "mean"]).round(0)
print(by_ram)
        count        mean
ram_gb
4.0        36  23722222.0
8.0        89  25503371.0
16.0       70  27634286.0
32.0       18  36022222.0

حافظه هم اثر دارد و یکنواخت بالا می‌رود — از ۲۳٫۷ میلیون برای ۴ گیگ تا ۳۶ میلیون برای ۳۲ گیگ.

ولی یادت باشد ram_gb بیست‌وشش خانهٔ خالی داشت. این جدول آن ردیف‌ها را بی‌سروصدا کنار گذاشتهgroupby مقدارهای خالی را نادیده می‌گیرد. جمعِ ستونِ count می‌شود ۲۱۳، نه ۲۳۸.

⚠️ مواظب باش: این یکی از بی‌سروصداترین رفتارهای pandas است. هیچ هشداری نمی‌دهد و هیچ خطایی نمی‌سازد؛ فقط ۱۱ درصدِ داده‌ات در تحلیل نیست. بعد از هر groupby، ستونِ count را جمع بزن و با len(df) مقایسه کن. اگر نخواند، بدان چرا.

۶. گزارشِ نهایی#

هر تحلیل باید در چند خط قابلِ خواندن باشد. الگوی ثابت:

lines = [
    f"داده: {report['rows_in']} ردیف خام، {report['rows_out']} ردیف پس از پاک‌سازی.",
    f"حذف‌شده: {report['duplicates_removed']} تکراری، {report['impossible_years_removed']} سال غیرممکن.",
    f"ناقص: {report['ram_missing']} ردیف بدون مقدارِ حافظه (در تحلیلِ حافظه کنار گذاشته شدند).",
    "یافته ۱ — سالِ ساخت قوی‌ترین عاملِ قیمت است (همبستگی ۰٫۸۹۵ بدون اپل).",
    "یافته ۲ — اپل یک زیرگروهِ جداست: میانگینِ حدود ۱٫۹ برابر و همهٔ ۹ مقدارِ پرت.",
    "یافته ۳ — شهر اثرِ قابلِ‌اتکایی ندارد (اختلافِ ۱۸ درصد، با نمونهٔ کوچک).",
    "محدودیت: داده ساختگی است و فقط سال، حافظه، برند و شهر را دارد؛ هیچ مشخصهٔ",
    "دیگری (پردازنده، وضعیت ظاهری) در آن نیست، پس هر تفاوتِ قیمت می‌تواند از",
    "همان‌ها بیاید. نتیجه‌ها دربارهٔ همین دیتاست‌اند، نه دربارهٔ بازار.",
]
for line in lines:
    print(line)
داده: 244 ردیف خام، 238 ردیف پس از پاک‌سازی.
حذف‌شده: 4 تکراری، 2 سال غیرممکن.
ناقص: 26 ردیف بدون مقدارِ حافظه (در تحلیلِ حافظه کنار گذاشته شدند).
یافته ۱ — سالِ ساخت قوی‌ترین عاملِ قیمت است (همبستگی ۰٫۸۹۵ بدون اپل).
یافته ۲ — اپل یک زیرگروهِ جداست: میانگینِ حدود ۱٫۹ برابر و همهٔ ۹ مقدارِ پرت.
یافته ۳ — شهر اثرِ قابلِ‌اتکایی ندارد (اختلافِ ۱۸ درصد، با نمونهٔ کوچک).
محدودیت: داده ساختگی است و فقط سال، حافظه، برند و شهر را دارد؛ هیچ مشخصهٔ
دیگری (پردازنده، وضعیت ظاهری) در آن نیست، پس هر تفاوتِ قیمت می‌تواند از
همان‌ها بیاید. نتیجه‌ها دربارهٔ همین دیتاست‌اند، نه دربارهٔ بازار.

سه بخش دارد و هر سه اجباری‌اند: داده (چه بود، چه شد)، یافته‌ها (هر کدام با عددش)، و محدودیت (چه چیزی را نمی‌شود از این نتیجه گرفت).

بخشِ سوم همان چیزی است که همه جا می‌اندازند و همان چیزی است که گزارش را قابلِ‌اعتماد می‌کند. گزارشی که محدودیت ندارد، یا کارِ کمی روی داده شده یا نویسنده‌اش آن‌ها را نمی‌بیند — و خواننده نمی‌داند کدام.

🔧 اگر کار نکرد: اگر عددهای گزارشت با اینجا فرق کردند، اول report را چاپ کن. اگر rows_out تو ۲۳۸ نیست، یکی از قدم‌های پاک‌سازی جا افتاده یا ترتیبش عوض شده — و ترتیب مهم است: حذفِ تکراری قبل از تبدیلِ قیمت، چون ردیف‌های تکراری هم باید یک بار تبدیل شوند نه چند بار.

🤖 از دستیارت بپرس: گزارشِ نهایی‌ات را برایش بگذار و بپرس: «یک خوانندهٔ بدبین به این گزارش چه ایرادهایی می‌گیرد؟ فقط فهرست کن.» هر ایرادی را که گرفت خودت روی داده بسنج. بعضی‌شان درست‌اند و باید به بخشِ «محدودیت» اضافه شوند؛ بعضی‌شان را داده رد می‌کند و همان رد کردن، گزارشت را محکم‌تر می‌کند.

تمرین‌ها

اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.

در فصل بعد#

ترمِ ۲ تمام شد. حالا می‌توانی یک دیتاستِ واقعی را از فایلِ خام تا یک گزارشِ قابلِ‌دفاع ببری — و این خودش یک مهارتِ کاملِ شغلی است.

ولی تا اینجا همیشه دربارهٔ گذشته حرف زده‌ای: این داده چه می‌گوید. ترمِ ۳ سؤالِ تازه‌ای می‌پرسد: آیا می‌توانی چیزی را که ندیده‌ای پیش‌بینی کنی؟ یک لپ‌تاپِ تازه با این مشخصات، چند قیمت می‌خورد؟ آنجا اولین مدلت را می‌سازی — و بلافاصله یاد می‌گیری چرا عددی که مدل به تو می‌دهد، تا وقتی درست اندازه‌گیری نشده، هیچ ارزشی ندارد.

به آخر این فصل رسیدی!

اگر ساختی و جواب داد، این دکمه مال توست.