{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "ccd8d984",
   "metadata": {},
   "source": [
    "# Pandas Practice Questions (Instructor Solutions)\n",
    "\n",
    "This notebook contains **20 comprehensive Python pandas practice problems** organized in two sections:\n",
    "\n",
    "**Section A - Short Coding Questions (Questions 1-17):**\n",
    "- Questions 1-12: Basic pandas operations (loading, selection, filtering, handling missing values)\n",
    "- Questions 13-17: Short coding questions on duplicates, missing values, column creation, filtering, and statistics\n",
    "\n",
    "**Section B - Applied Coding Questions (Questions 18-20):**\n",
    "- Question 18: GroupBy with multiple aggregations\n",
    "- Question 19: Advanced filtering and column creation\n",
    "- Question 20: Handling missing values and outliers\n",
    "\n",
    "Each question includes:\n",
    "- Clear problem description\n",
    "- Hints for solving\n",
    "- Multiple-choice code options (where applicable)\n",
    "- Instructor solution with inline examples\n",
    "- Test cases using small DataFrames"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "570dad49",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "from io import StringIO"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "d2a79a76",
   "metadata": {},
   "outputs": [],
   "source": [
    "name = 'student name'\n",
    "roll_number = 'student roll number'"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cc7b215f",
   "metadata": {},
   "source": [
    "### 1. Load a CSV string into a DataFrame\n",
    "**Return:** A pandas DataFrame from the CSV string\n",
    "\n",
    "**Choose the correct line:**\n",
    "- (a) `return pd.read_excel(StringIO(csv_string))`\n",
    "- (b) `return pd.read_csv(StringIO(csv_string))`\n",
    "- (c) `return pd.DataFrame(csv_string.split('\\n'))`\n",
    "- (d) `return csv_string.to_dataframe()`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "ef841c30",
   "metadata": {},
   "outputs": [],
   "source": [
    "def load_csv_string(csv_string: str) -> pd.DataFrame:\n",
    "    return pd.read_csv(StringIO(csv_string))\n",
    "\n",
    "# Test data\n",
    "csv_data = \"name,age,score\\nAlice,25,85\\nBob,30,90\\nCharlie,22,78\"\n",
    "# df = load_csv_string(csv_data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "06879135",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "csv_data = \"name,age,score\\nAlice,25,85\\nBob,30,90\\nCharlie,22,78\"\n",
    "df = load_csv_string(csv_data)\n",
    "assert isinstance(df, pd.DataFrame)\n",
    "assert list(df.columns) == ['name', 'age', 'score']\n",
    "assert df.shape == (3, 3)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "32af94a5",
   "metadata": {},
   "source": [
    "### 2. Get shape and column names\n",
    "**Return:** A tuple of (number of rows, number of columns, list of column names)\n",
    "\n",
    "**Choose the correct code:**\n",
    "- (a) `return (df.size, df.ndim, df.columns)`\n",
    "- (b) `return (df.shape[0], df.shape[1], list(df.columns))`\n",
    "- (c) `return df.info()`\n",
    "- (d) `return (len(df), len(df.index), df.to_list())`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "d819f154",
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_dataframe_info(df: pd.DataFrame) -> tuple:\n",
    "    rows, cols = df.shape\n",
    "    columns = list(df.columns)\n",
    "    return (rows, cols, columns)\n",
    "\n",
    "# get_dataframe_info(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "04da7f29",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "csv_data = \"x,y,z\\n1,2,3\\n4,5,6\"\n",
    "df = load_csv_string(csv_data)\n",
    "rows, cols, columns = get_dataframe_info(df)\n",
    "assert rows == 2\n",
    "assert cols == 3\n",
    "assert columns == ['x', 'y', 'z']"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c22e3b52",
   "metadata": {},
   "source": [
    "### 3. Get the first n rows of a DataFrame\n",
    "**Return:** DataFrame containing first n rows\n",
    "\n",
    "**Choose the correct code:**\n",
    "- (a) `return df.iloc[:n]`\n",
    "- (b) `return df.head(n)`\n",
    "- (c) `return df.nlargest(n, axis=0)`\n",
    "- (d) `return df[:n:1]`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "a7c6666a",
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_first_n_rows(df: pd.DataFrame, n: int) -> pd.DataFrame:\n",
    "    return df.head(n)\n",
    "\n",
    "# get_first_n_rows(df, 2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "94270e73",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "csv_data = \"a,b\\n1,10\\n2,20\\n3,30\\n4,40\"\n",
    "df = load_csv_string(csv_data)\n",
    "first_two = get_first_n_rows(df, 2)\n",
    "assert first_two.shape == (2, 2)\n",
    "assert first_two['a'].tolist() == [1, 2]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2fd74d3f",
   "metadata": {},
   "source": [
    "### 4. Get basic statistics for numeric columns\n",
    "**Return:** A pandas DataFrame with descriptive statistics (using .describe())\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "18326f48",
   "metadata": {},
   "outputs": [],
   "source": [
    "def describe_numeric(df: pd.DataFrame) -> pd.DataFrame:\n",
    "    return df.describe()\n",
    "\n",
    "# describe_numeric(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a33beb99",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "csv_data = \"val1,val2\\n10,100\\n20,200\\n30,300\"\n",
    "df = load_csv_string(csv_data)\n",
    "stats = describe_numeric(df)\n",
    "assert isinstance(stats, pd.DataFrame)\n",
    "assert 'count' in stats.index\n",
    "assert 'mean' in stats.index\n",
    "assert 'std' in stats.index"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "93b7e373",
   "metadata": {},
   "source": [
    "### 5. Select a single column as a Series\n",
    "**Return:** A pandas Series for the specified column"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "46bac4c4",
   "metadata": {},
   "outputs": [],
   "source": [
    "def select_column(df: pd.DataFrame, col_name: str) -> pd.Series:\n",
    "    return df[col_name]\n",
    "\n",
    "# select_column(df, 'age')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "a9573a5a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "csv_data = \"name,age\\nAlice,25\\nBob,30\\nCharlie,22\"\n",
    "df = load_csv_string(csv_data)\n",
    "age_series = select_column(df, 'age')\n",
    "assert isinstance(age_series, pd.Series)\n",
    "assert age_series.tolist() == [25, 30, 22]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cdf36936",
   "metadata": {},
   "source": [
    "### 6. Filter rows where a column value exceeds a threshold\n",
    "**Return:** A DataFrame containing only rows where column > threshold\n",
    "\n",
    "**Hint:** Use boolean indexing `df[df[col_name] > threshold]` and `.reset_index(drop=True)` to reset row indices.\n",
    "\n",
    "**Choose the correct code:**\n",
    "- (a) `return df.filter(column=col_name, value=threshold)`\n",
    "- (b) `return df.loc[df[col_name] > threshold]`\n",
    "- (c) `return df[df[col_name] > threshold].reset_index(drop=True)`\n",
    "- (d) `return df.query(f'{col_name} > {threshold}')`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dcf00b4e",
   "metadata": {},
   "outputs": [],
   "source": [
    "def filter_by_threshold(df: pd.DataFrame, col_name: str, threshold: float) -> pd.DataFrame:\n",
    "    return df[df[col_name] > threshold].reset_index(drop=True)\n",
    "\n",
    "# filter_by_threshold(df, 'age', 25)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "41b27d4e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "csv_data = \"name,score\\nAlice,85\\nBob,92\\nCharlie,78\\nDiana,88\"\n",
    "df = load_csv_string(csv_data)\n",
    "filtered = filter_by_threshold(df, 'score', 80)\n",
    "assert filtered.shape[0] == 3\n",
    "assert filtered['score'].min() > 80"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e70c9d29",
   "metadata": {},
   "source": [
    "### 7. Count missing (NaN) values in each column\n",
    "**Return:** A pandas Series with column names as index and count of NaN as values\n",
    "\n",
    "**Hint:** Use `.isnull().sum()` to count missing values in each column."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eb46b189",
   "metadata": {},
   "outputs": [],
   "source": [
    "def count_missing_values(df: pd.DataFrame) -> pd.Series:\n",
    "    return df.isnull().sum()\n",
    "\n",
    "# count_missing_values(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "64e94a4d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "data = {'a': [1, 2, np.nan], 'b': [4, np.nan, np.nan], 'c': [7, 8, 9]}\n",
    "df = pd.DataFrame(data)\n",
    "missing = count_missing_values(df)\n",
    "assert missing['a'] == 1\n",
    "assert missing['b'] == 2\n",
    "assert missing['c'] == 0"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2efdd80f",
   "metadata": {},
   "source": [
    "### 8. Drop rows containing any NaN values\n",
    "**Return:** A DataFrame with all rows containing NaN removed\n",
    "\n",
    "**Hint:** Use `.dropna()` to remove rows with missing values, then `.reset_index(drop=True)` to renumber rows."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "81bea45f",
   "metadata": {},
   "outputs": [],
   "source": [
    "def drop_rows_with_nan(df: pd.DataFrame) -> pd.DataFrame:\n",
    "    return df.dropna().reset_index(drop=True)\n",
    "\n",
    "# drop_rows_with_nan(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7aba5bcf",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "data = {'x': [1, 2, np.nan], 'y': [10, np.nan, 30]}\n",
    "df = pd.DataFrame(data)\n",
    "clean_df = drop_rows_with_nan(df)\n",
    "assert clean_df.shape[0] == 1\n",
    "assert clean_df['x'].iloc[0] == 1\n",
    "assert clean_df['y'].iloc[0] == 10"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d0ffbdd4",
   "metadata": {},
   "source": [
    "### 9. Fill missing values with the mean of the column\n",
    "**Return:** A DataFrame where NaN values in numeric columns are replaced by column mean\n",
    "\n",
    "**Hint:** Get numeric columns using `.select_dtypes()`, then use `.fillna()` with the column mean."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "98dc4cae",
   "metadata": {},
   "outputs": [],
   "source": [
    "def fill_missing_with_mean(df: pd.DataFrame) -> pd.DataFrame:\n",
    "    df_copy = df.copy()\n",
    "    numeric_cols = df_copy.select_dtypes(include=[np.number]).columns\n",
    "    for col in numeric_cols:\n",
    "        df_copy[col].fillna(df_copy[col].mean(), inplace=True)\n",
    "    return df_copy\n",
    "\n",
    "# data = {'val': [10, 20, np.nan, 30]}\n",
    "# df = pd.DataFrame(data)\n",
    "# fill_missing_with_mean(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "89401349",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "data = {'val': [10, 20, np.nan, 30]}\n",
    "df = pd.DataFrame(data)\n",
    "filled = fill_missing_with_mean(df)\n",
    "assert filled['val'].isnull().sum() == 0\n",
    "assert filled['val'].iloc[2] == 20"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a799ecd6",
   "metadata": {},
   "source": [
    "### 10. Group by a column and calculate the mean of another column\n",
    "**Return:** A DataFrame with grouped results (group column and mean)\n",
    "\n",
    "**Hint:** Use `.groupby(group_col)[agg_col].mean()` and `.reset_index()` to convert to DataFrame."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9eb9b6a6",
   "metadata": {},
   "outputs": [],
   "source": [
    "def group_by_mean(df: pd.DataFrame, group_col: str, agg_col: str) -> pd.DataFrame:\n",
    "    result = df.groupby(group_col)[agg_col].mean().reset_index()\n",
    "    return result\n",
    "\n",
    "# data = {'category': ['A', 'B', 'A', 'B'], 'value': [10, 20, 30, 40]}\n",
    "# df = pd.DataFrame(data)\n",
    "# group_by_mean(df, 'category', 'value')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "4c2afdf1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "data = {'category': ['A', 'B', 'A', 'B'], 'value': [10, 20, 30, 40]}\n",
    "df = pd.DataFrame(data)\n",
    "grouped = group_by_mean(df, 'category', 'value')\n",
    "assert grouped.shape[0] == 2\n",
    "assert grouped.loc[grouped['category'] == 'A', 'value'].iloc[0] == 20\n",
    "assert grouped.loc[grouped['category'] == 'B', 'value'].iloc[0] == 30"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9bf612b8",
   "metadata": {},
   "source": [
    "### 11. Merge two DataFrames on a common column\n",
    "**Return:** A merged DataFrame (inner join on the specified key)\n",
    "\n",
    "**Choose the correct code:**\n",
    "- (a) `return left.join(right, on=on)`\n",
    "- (b) `return pd.concat([left, right])`\n",
    "- (c) `return pd.merge(left, right, on=on, how='inner')`\n",
    "- (d) `return left.combine(right)`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "7fc876a4",
   "metadata": {},
   "outputs": [],
   "source": [
    "def merge_dataframes(left: pd.DataFrame, right: pd.DataFrame, on: str) -> pd.DataFrame:\n",
    "    return pd.merge(left, right, on=on, how='inner')\n",
    "\n",
    "# merge_dataframes(left_df, right_df, 'id')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "55d87bbe",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "left = pd.DataFrame({'id': [1, 2, 3], 'value_left': [10, 20, 30]})\n",
    "right = pd.DataFrame({'id': [2, 3, 4], 'value_right': [200, 300, 400]})\n",
    "merged = merge_dataframes(left, right, 'id')\n",
    "assert merged.shape[0] == 2\n",
    "assert set(merged.columns) == {'id', 'value_left', 'value_right'}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c4a54a39",
   "metadata": {},
   "source": [
    "### 12. Convert a column to datetime format\n",
    "**Return:** A DataFrame where the specified column has been converted to datetime\n",
    "\n",
    "**Choose the correct code:**\n",
    "- (a) `df_copy[col_name] = df_copy[col_name].astype(datetime)`\n",
    "- (b) `df_copy[col_name] = pd.to_datetime(df_copy[col_name])`\n",
    "- (c) `df_copy[col_name].convert_to_datetime()`\n",
    "- (d) `df_copy[col_name] = datetime.strptime(df_copy[col_name], '%Y-%m-%d')`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2d78dfe7",
   "metadata": {},
   "outputs": [],
   "source": [
    "def convert_to_datetime(df: pd.DataFrame, col_name: str) -> pd.DataFrame:\n",
    "    df_copy = df.copy()\n",
    "    df_copy[col_name] = pd.to_datetime(df_copy[col_name])\n",
    "    return df_copy\n",
    "\n",
    "# convert_to_datetime(df, 'date_column')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "83e8756c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "data = {'date': ['2023-01-15', '2023-02-20', '2023-03-25']}\n",
    "df = pd.DataFrame(data)\n",
    "converted = convert_to_datetime(df, 'date')\n",
    "assert pd.api.types.is_datetime64_any_dtype(converted['date'])\n",
    "assert converted['date'].iloc[0].year == 2023\n",
    "assert converted['date'].iloc[0].month == 1"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "42b7f823",
   "metadata": {},
   "source": [
    "### 13. Drop Duplicate Rows\n",
    "You have a DataFrame with duplicate rows. The command `drop_duplicates` on subset of columns named `['Name', 'Team']` is to be used."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "737538a6",
   "metadata": {},
   "outputs": [],
   "source": [
    "def drop_duplicates_by_cols(df: pd.DataFrame) -> pd.DataFrame:\n",
    "    \"\"\"Remove duplicate rows based on Name and Team columns.\"\"\"\n",
    "    return df.drop_duplicates(subset=['Name', 'Team'])\n",
    "\n",
    "# Example\n",
    "sample_df = pd.DataFrame({\n",
    "    'Name': ['Alice', 'Bob', 'Alice', 'Charlie'],\n",
    "    'Team': ['X', 'Y', 'X', 'Z'],\n",
    "    'Salary': [50000, 55000, 50000, 60000]\n",
    "})\n",
    "# result = drop_duplicates_by_cols(sample_df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "c68730fd",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "sample_df = pd.DataFrame({\n",
    "    'Name': ['Alice', 'Bob', 'Alice', 'Charlie'],\n",
    "    'Team': ['X', 'Y', 'X', 'Z'],\n",
    "    'Salary': [50000, 55000, 50000, 60000]\n",
    "})\n",
    "result = drop_duplicates_by_cols(sample_df)\n",
    "assert result.shape[0] == 3  # One duplicate removed\n",
    "assert list(result['Name']) == ['Alice', 'Bob', 'Charlie']"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e8b7e9cd",
   "metadata": {},
   "source": [
    "### 14. Fill Missing Values in a Column\n",
    "Write a Python command to fill all missing values in the column 'College' with the text 'Unknown'."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "21851dea",
   "metadata": {},
   "outputs": [],
   "source": [
    "def fill_missing_college(df: pd.DataFrame) -> pd.DataFrame:\n",
    "    \"\"\"Fill missing values in College column with 'Unknown'.\"\"\"\n",
    "    df_copy = df.copy()\n",
    "    df_copy['College'] = df_copy['College'].fillna('Unknown')\n",
    "    return df_copy\n",
    "\n",
    "# Example\n",
    "sample_df = pd.DataFrame({\n",
    "    'Name': ['Alice', 'Bob', 'Charlie'],\n",
    "    'College': ['IIT', None, 'NIT']\n",
    "})\n",
    "# result = fill_missing_college(sample_df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1c034989",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "sample_df = pd.DataFrame({\n",
    "    'Name': ['Alice', 'Bob', 'Charlie'],\n",
    "    'College': ['IIT', None, 'NIT']\n",
    "})\n",
    "result = fill_missing_college(sample_df)\n",
    "assert result['College'].isnull().sum() == 0\n",
    "assert result['College'].tolist() == ['IIT', 'Unknown', 'NIT']"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "95917896",
   "metadata": {},
   "source": [
    "### 15. Create New Column with Percentage Increase\n",
    "Given a DataFrame df with a 'Salary' column, write code to increase salary by 5% and store it in a new column 'UpdatedSalary'.\n",
    "\n",
    "**Hint:** Multiply the Salary column by 1.05 to increase by 5%.\n",
    "\n",
    "**Choose the correct code:**\n",
    "- (a) `df['UpdatedSalary'] = df['Salary'] * 5`\n",
    "- (b) `df['UpdatedSalary'] = df['Salary'] * 1.05`\n",
    "- (c) `df['UpdatedSalary'] = df['Salary'] + 0.05`\n",
    "- (d) `df['UpdatedSalary'] = df['Salary'].apply(lambda x: x * 5)`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "03a08b07",
   "metadata": {},
   "outputs": [],
   "source": [
    "def add_updated_salary(df: pd.DataFrame) -> pd.DataFrame:\n",
    "    \"\"\"Create UpdatedSalary column with 5% increase.\"\"\"\n",
    "    df_copy = df.copy()\n",
    "    df_copy['UpdatedSalary'] = df_copy['Salary'] * 1.05\n",
    "    return df_copy\n",
    "\n",
    "# Example\n",
    "sample_df = pd.DataFrame({\n",
    "    'Name': ['Alice', 'Bob', 'Charlie'],\n",
    "    'Salary': [50000, 55000, 60000]\n",
    "})\n",
    "# result = add_updated_salary(sample_df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a30e234e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "sample_df = pd.DataFrame({\n",
    "    'Name': ['Alice', 'Bob', 'Charlie'],\n",
    "    'Salary': [50000, 55000, 60000]\n",
    "})\n",
    "result = add_updated_salary(sample_df)\n",
    "assert 'UpdatedSalary' in result.columns\n",
    "assert result['UpdatedSalary'].iloc[0] == 52500\n",
    "assert result['UpdatedSalary'].iloc[1] == 57750"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0ecd58c7",
   "metadata": {},
   "source": [
    "### 16. Filter Rows with Range Condition\n",
    "Write Python code to select rows where 'Profit' is between 30 and 55 (inclusive).\n",
    "\n",
    "**Hint:** Use boolean indexing with AND operator `&`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fa937d6f",
   "metadata": {},
   "outputs": [],
   "source": [
    "def filter_profit_range(df: pd.DataFrame) -> pd.DataFrame:\n",
    "    \"\"\"Filter rows where Profit is between 30 and 55 inclusive.\"\"\"\n",
    "    return df[(df['Profit'] >= 30) & (df['Profit'] <= 55)].reset_index(drop=True)\n",
    "\n",
    "# Example\n",
    "sample_df = pd.DataFrame({\n",
    "    'Name': ['A', 'B', 'C', 'D'],\n",
    "    'Profit': [25, 40, 60, 35]\n",
    "})\n",
    "# result = filter_profit_range(sample_df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1d76a0d1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "sample_df = pd.DataFrame({\n",
    "    'Name': ['A', 'B', 'C', 'D'],\n",
    "    'Profit': [25, 40, 60, 35]\n",
    "})\n",
    "result = filter_profit_range(sample_df)\n",
    "assert result.shape[0] == 2\n",
    "assert list(result['Name']) == ['B', 'D']\n",
    "assert all((result['Profit'] >= 30) & (result['Profit'] <= 55))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4d602461",
   "metadata": {},
   "source": [
    "### 17. Get Summary Statistics\n",
    "Write a Python command to show summary statistics (mean, median, std, min, max, etc.) for the entire DataFrame."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6053e461",
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_summary_stats(df: pd.DataFrame) -> pd.DataFrame:\n",
    "    \"\"\"Return summary statistics for numeric columns.\"\"\"\n",
    "    return df.describe()\n",
    "\n",
    "# Example\n",
    "sample_df = pd.DataFrame({\n",
    "    'Age': [25, 30, 28, 35],\n",
    "    'Salary': [50000, 55000, 52000, 60000]\n",
    "})\n",
    "# stats = get_summary_stats(sample_df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ec38cffb",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "sample_df = pd.DataFrame({\n",
    "    'Age': [25, 30, 28, 35],\n",
    "    'Salary': [50000, 55000, 52000, 60000]\n",
    "})\n",
    "stats = get_summary_stats(sample_df)\n",
    "assert isinstance(stats, pd.DataFrame)\n",
    "assert 'mean' in stats.index\n",
    "assert 'std' in stats.index\n",
    "assert stats.loc['count', 'Age'] == 4"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d320b53",
   "metadata": {},
   "source": [
    "### 18. GroupBy with Multiple Aggregations\n",
    "You have a DataFrame with columns: Name, Team, Salary, Profit\n",
    "\n",
    "Write Python code to:\n",
    "1. Group the data by Team\n",
    "2. aggregate average salary and total profit for each team\n",
    "3. return the result\n",
    "\n",
    "**Hint:** Use `.groupby()` with `.agg()` for multiple aggregations.\n",
    "\n",
    "**Choose the correct code:**\n",
    "- (a) `df.groupby('Team').agg({'Salary': 'mean', 'Profit': 'sum'})`\n",
    "- (b) `df.groupby('Team')[['Salary', 'Profit']].agg(['mean', 'sum'])`\n",
    "- (c) `df.group('Team').apply(lambda x: {'avg_salary': x['Salary'].mean(), 'total_profit': x['Profit'].sum()})`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "51fd5a32",
   "metadata": {},
   "outputs": [],
   "source": [
    "def groupby_team_agg(df: pd.DataFrame) -> pd.DataFrame:\n",
    "    \"\"\"Group by Team and calculate average Salary and total Profit.\"\"\"\n",
    "    result = df.groupby('Team').agg({\n",
    "        'Salary': 'mean',\n",
    "        'Profit': 'sum'\n",
    "    }).reset_index()\n",
    "    result.columns = ['Team', 'AvgSalary', 'TotalProfit']\n",
    "    return result\n",
    "\n",
    "# Example\n",
    "sample_df = pd.DataFrame({\n",
    "    'Name': ['A', 'B', 'C', 'D'],\n",
    "    'Team': ['X', 'X', 'Y', 'Y'],\n",
    "    'Salary': [50000, 55000, 52000, 53000],\n",
    "    'Profit': [45, 30, 60, 25]\n",
    "})\n",
    "# result = groupby_team_agg(sample_df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5311d7b7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "sample_df = pd.DataFrame({\n",
    "    'Name': ['A', 'B', 'C', 'D'],\n",
    "    'Team': ['X', 'X', 'Y', 'Y'],\n",
    "    'Salary': [50000, 55000, 52000, 53000],\n",
    "    'Profit': [45, 30, 60, 25]\n",
    "})\n",
    "result = groupby_team_agg(sample_df)\n",
    "assert result.shape[0] == 2\n",
    "assert 'AvgSalary' in result.columns\n",
    "assert result.loc[result['Team'] == 'X', 'AvgSalary'].iloc[0] == 52500\n",
    "assert result.loc[result['Team'] == 'X', 'TotalProfit'].iloc[0] == 75"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "129962e0",
   "metadata": {},
   "source": [
    "### 19. Advanced Filtering and Column Creation\n",
    "Given a DataFrame with columns: Name, Score1, Score2\n",
    "\n",
    "Write Python code to:\n",
    "1. Select only rows where Score1 > 40 AND Score2 > 50\n",
    "2. Create a new column AverageScore = mean of Score1 and Score2\n",
    "3. return dataframe with only the  `[['Name', 'AverageScore']]`\n",
    "**Hint:** Filter first using boolean indexing, then add the new column, then select specific columns."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a1e8de3a",
   "metadata": {},
   "outputs": [],
   "source": [
    "def advanced_filter_and_create(df: pd.DataFrame) -> pd.DataFrame:\n",
    "    \"\"\"Filter rows and create average score column, return Name and AverageScore.\"\"\"\n",
    "    df_copy = df.copy()\n",
    "    # Filter rows where Score1 > 40 AND Score2 > 50\n",
    "    df_filtered = df_copy[(df_copy['Score1'] > 40) & (df_copy['Score2'] > 50)]\n",
    "    # Create AverageScore column\n",
    "    df_filtered['AverageScore'] = (df_filtered['Score1'] + df_filtered['Score2']) / 2\n",
    "    # Select only Name and AverageScore\n",
    "    result = df_filtered[['Name', 'AverageScore']].reset_index(drop=True)\n",
    "    return result\n",
    "\n",
    "# Example\n",
    "sample_df = pd.DataFrame({\n",
    "    'Name': ['A', 'B', 'C', 'D'],\n",
    "    'Score1': [40, 55, 70, 30],\n",
    "    'Score2': [50, 65, 75, 35]\n",
    "})\n",
    "# result = advanced_filter_and_create(sample_df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "502a3655",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "sample_df = pd.DataFrame({\n",
    "    'Name': ['A', 'B', 'C', 'D'],\n",
    "    'Score1': [40, 55, 70, 30],\n",
    "    'Score2': [50, 65, 75, 35]\n",
    "})\n",
    "result = advanced_filter_and_create(sample_df)\n",
    "assert result.shape[0] == 2  # Only B and C qualify\n",
    "assert list(result['Name']) == ['B', 'C']\n",
    "assert result['AverageScore'].iloc[0] == 60  # (55+65)/2\n",
    "assert result['AverageScore'].iloc[1] == 72.5  # (70+75)/2"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9ef8914e",
   "metadata": {},
   "source": [
    "### 20. Handle Missing Values and Outliers\n",
    "You have a DataFrame with an 'Age' column containing missing values and outliers (Age > 100).\n",
    "\n",
    "Write Python code to:\n",
    "1. Replace missing values with the median age\n",
    "2. Remove rows where Age > 100\n",
    "3. Return the cleaned DataFrame\n",
    "\n",
    "**Hint:** Use `.fillna()` with median, then filter with boolean indexing."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d46a7900",
   "metadata": {},
   "outputs": [],
   "source": [
    "def clean_age_data(df: pd.DataFrame) -> pd.DataFrame:\n",
    "    \"\"\"Replace missing Age values with median, remove Age > 100.\"\"\"\n",
    "    df_copy = df.copy()\n",
    "    # Replace missing values with median age\n",
    "    median_age = df_copy['Age'].median()\n",
    "    df_copy['Age'] = df_copy['Age'].fillna(median_age)\n",
    "    # Remove rows where Age > 100\n",
    "    df_clean = df_copy[df_copy['Age'] <= 100].reset_index(drop=True)\n",
    "    return df_clean\n",
    "\n",
    "# Example\n",
    "sample_df = pd.DataFrame({\n",
    "    'Name': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'],\n",
    "    'Age': [25, np.nan, 105, 30, np.nan]\n",
    "})\n",
    "# result = clean_age_data(sample_df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "53f1ce09",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assertions\n",
    "sample_df = pd.DataFrame({\n",
    "    'Name': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'],\n",
    "    'Age': [25, np.nan, 105, 30, np.nan]\n",
    "})\n",
    "result = clean_age_data(sample_df)\n",
    "assert result['Age'].isnull().sum() == 0  # No missing values\n",
    "assert (result['Age'] <= 100).all()  # No outliers\n",
    "assert result.shape[0] == 4  # Charlie (105) removed\n",
    "assert list(result['Name']) == ['Alice', 'Bob', 'Diana', 'Eve']"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv (3.13.7)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
