题目来源 leetcode 上的 30 天 Pandas 挑战30 天 Pandas 挑战,本文作为一个打卡记录来学习 pandas 基本用法。

pandas 基础

主要从 Joyful Pandas 以及 Pandas 官方中文文档 上学习的 pandas

数据结构

首先是基本数据结构 SeriesDataFrame,其中 DataFrame 的每一行或每一列都是一个 Series。基本的属性看名字就知道了:values, index, columns, dtypes, shape,用于描述和展示的函数 info, describe, head, tail

基本函数

去重

  1. unique 去除重复
  2. nunique 计算唯一值个数的函数
  3. value_counts 统计频次

但是 unique 只能用在 Series

  1. duplicated 返回的是否为唯一值的布尔列表
  2. drop_duplicates 是去除这些重复的列

这两个函数都可以使用一个 keep 的参数,该参数决定保留重复的哪一项(默认是保留出现的第一次,keep='last' 则保留出现的最后一个,keep=false 则全不保留,去重时可以使用 subset 来决定考虑哪些列。

修改属性

修改索引名的函数 rename(columns=dict),其中字典的格式是 {'pre_name': 'new_name'}

排序

  1. sort_values 根据属性值排序
  2. sort_index 根据索引值排序

默认 ascending=True

rank 方法是对某一属性计算排名,有多种排序方法,比如method='dense' 或者 'first'

转换类型

tolist 将 Series 转换成列表,因此也可以用于将 DataFrame 的一列转换成列表

pandas 索引

1. 序列 s 的索引

使用 s[item] 或者 s[一个列表]

2. DataFrame df 的列索引

使用 df[列名](等价于 df.列名 ) 或者 df[列名的列表](以下不包含列表形式,同理即可)

3.dfloc 索引

使用 df.loc[*, *],其中 * 可以是元素,元素的列表,元素的切片,布尔列表及函数,第二个 * 可以省略,表示取所有列。主要讲后三个

loc 的切片和普通的切片不同,是包含两个端点的,比如 df.loc['yjj' : 'psy'] 这个切片是包含 yjjpsy 这两行的

传入 loc 的布尔列表长度应该和 df 长度相同,然后选出布尔列表中为 true 的部分

使用函数时,返回值必须为元素,元素列表,元素切片或布尔列表。对于 df.loc[func] 事实上是 df.loc[func(df)]

一个细节是第二个 * 如果是单一的元素,比如 loc[:, 'age'],则取出的是一个 Series,如果是元素列表,则取出的是 DataFrame,比如 loc[:, ['age']] 取出的是只有一列的 DataFrame

4. dfiloc 索引

这种索引方式和 loc 索引基本一致,除了 iloc 索引的是位置的序号,loc 索引的是元素。

索引运算

四种运算:intersection, union, difference, symmetric_difference

字符串

str 属性是对 IndexSeries 上的字符串类型的访问器,可以批量处理数据,比如 df['name'].str.upper() 就是对 name 这一列的所有字符串进行大写化。

连接

关系连接

连接的概念和数据库中的 join 概念是一样的。通过 on 指定连接的键,how 指定连接方法。各种连接方式为

连接方式

对两个表中的同一个键,连接采用的是笛卡尔积的方式。on 可以连接多个参数,比如 on=[class, name];也可以连接不同名字的键,比如 left_on='df1_name', right_on='df2_name', how='left';重复的列名,可以通过 suffixes 添加列名的后缀,比如 suffixes=['_Math', '_Chinese']

df1.join(df2, how='left')
# join 默认索引连接,merge 可以 on 其他列

方向连接

concat 方法

分组

分组使用 groupby(分组依据),比如 groupby(['school', 'class']),该函数返回一个 gruopby 对象(一个键值对)

agg

agg 可以帮助我们对多个列进行不同的聚合,对一个列进行多种聚合,支持聚合后的列命名

  1. 一列采用多种聚合
    每一列都使用多种函数 grouped.agg(['sum', 'mean']),这种方法进行聚合时,比如 Age 列使用了 meanmax 两种统计方法,则生成的 DataFrame 是有多级索引的,这个例子中返回的分组 DataFrame 有这样的列

    |   Age  |
    |mean|max|
  2. 多列采用不同聚合
    每一列都有自己的聚合规则

    grouped.agg({
         'math_score': ['mean', 'max'],
         'physics_score': ['meam', 'min']
    })
  3. 聚合后重命名
    两种形式,第一种是在每一列选用的聚合函数的列表中,每个聚合函数改为一个元组,形式为 (名字, 函数),比如 'math_score': [('average_score', 'mean'), ('max_score', 'max')]

    第二种是形如 agg(新列名=('列名', '统计方法')),这种直接修改该列名字,比如

    df = activities.groupby('sell_date', as_index=False).agg(
        num_sold = ('product', 'nunique'),
        products = ('product', lambda x: x.to_list())
    )

transform

transform 和 agg

缺失值处理

dropna 方法能删除缺失值,包含参数 how, subset
isnanotna 可以用于布尔索引

题目

1 大的国家

595. 大的国家

解答

这种使用布尔列表进行索引就好了,lociloc 都可以使用布尔索引,但是一般我们使用 loc,因为 world.area >= 3000000 返回的是一个 Series 的数据

test = (world.area >= 3000000)
print(type(test))
# 输出是 <class 'pandas.core.series.Series'>

因此如果使用 iloc 索引时,由于必须索引整数,所以要写成 iloc[test.values],会更麻烦点。

import pandas as pd

def big_countries(world: pd.DataFrame) -> pd.DataFrame:
    condition1 = (world.area >= 3000000)
    condition2 = (world.population >= 25000000)
    columns = ['name', 'population', 'area']
    return world.loc[condition1 | condition2, columns]

2 可回收且低脂的产品

1757. 可回收且低脂的产品

解答

这个题目 1 是一样的原理,但是需要注意的是索引列时需要用 loc[condition, ['product_id']] 而不是 loc[condition, 'product_id']

import pandas as pd

def find_products(products: pd.DataFrame) -> pd.DataFrame:
    condition1 = (products.low_fats == 'Y')
    condition2 = (products.recyclable == 'Y')
    return products.loc[condition1 & condition2, ['product_id']]

3 从不订购的客户

183. 从不订购的客户

解答

这里需要找一个表的属性是否在另一个表中,使用的是 isin

import pandas as pd

def find_customers(customers: pd.DataFrame, orders: pd.DataFrame) -> pd.DataFrame:
    df = customers[~customers.id.isin(orders.customerId)]
    df = df.rename(columns={'name': 'Customers'})
    return df.loc[:, ['Customers']]

4 文章浏览 I

1148. 文章浏览 I

解答

import pandas as pd

def article_views(views: pd.DataFrame) -> pd.DataFrame: 
    condition  = (views.author_id == views.viewer_id)
    df = views[condition][['viewer_id']].rename(columns={'viewer_id': 'id'}).drop_duplicates(subset=['id']).sort_values('id')
    return df

5 无效的推文

1683. 无效的推文

解答

import pandas as pd

def invalid_tweets(tweets: pd.DataFrame) -> pd.DataFrame:
    condition = tweets.content.str.len() > 15
    df = tweets[condition][['tweet_id']]
    return df

6 计算特殊奖金

1873. 计算特殊奖金

解答

import pandas as pd

def calculate_special_bonus(employees: pd.DataFrame) -> pd.DataFrame:
    employees['bonus'] = employees.apply(
    lambda x: x['salary'] if x['employee_id'] % 2 and not x['name'].startswith('M') else 0, 
    axis=1
)

    df = employees[['employee_id', 'bonus']]
    return df.sort_values('employee_id')

7 修复表中的名字

1667. 修复表中的名字

解答

需要注意的是,每个字符串方法前,需要使用 .str 访问器。

def fix_names(users: pd.DataFrame) -> pd.DataFrame:
    users['name'] = users['name'].str[0].str.upper() + users['name'].str[1:].str.lower()
    df = users.sort_values('user_id')
    return df

8 查找拥有有效邮箱的用户

1517. 查找拥有有效邮箱的用户

解答

这一题需要复习正则表达式

def valid_emails(users: pd.DataFrame) -> pd.DataFrame:
    pattern = '^[a-zA-Z][a-zA-Z0-9_.-]*@leetcode\.com$'
    return users[users.mail.str.match(pattern)]

9 患某种疾病的患者

1527. 患某种疾病的患者

解答

这个题依然使用正则表达式解决,但是需要使用到单词边界的匹配。

import pandas as pd

def find_patients(patients: pd.DataFrame) -> pd.DataFrame:
    target = r'\bDIAB1'
    return patients[patients.conditions.str.contains(target)]

10 第N高的薪水

177. 第N高的薪水

解答

排序前需要去重

import pandas as pd

def nth_highest_salary(employee: pd.DataFrame, N: int) -> pd.DataFrame:
    df = employee[['salary']].drop_duplicates().sort_values('salary', ascending=False)
    if len(df) < N:
        return pd.DataFrame({f'getNthHighestSalary({N})': [None]})
    else:
        return pd.DataFrame({f'getNthHighestSalary({N})': [df.iloc[N-1]['salary']]})

11 第二高的薪水

176. 第二高的薪水

解答

没搞懂,这和上一题一样的。改成 2 即可。

12 部门工资最高的员工

184. 部门工资最高的员工

解答

import pandas as pd

def department_highest_salary(employee: pd.DataFrame, department: pd.DataFrame) -> pd.DataFrame:
    df = employee.merge(department, left_on='departmentId', right_on='id', how='inner', suffixes=['_e', '_d']).rename(columns={
        'name_d': "Department",
        'name_e': "Employee",
        'salary': 'Salary'
        })[['Department', 'Employee', 'Salary']]
    salary = df.groupby('Department')['Salary'].transform('max')
    df = df[df['Salary'] == salary]
    return df

13 分数排名

178. 分数排名

解答

import pandas as pd

def order_scores(scores: pd.DataFrame) -> pd.DataFrame:
    scores['rank'] = scores['score'].rank(method='dense', ascending=False)
    return scores[['score', 'rank']].sort_values('score', ascending=False)

14 删除重复的电子邮箱

196. 删除重复的电子邮箱

解答

这里涉及到 inplace 这个参数,这个参数设置为 True 时,相当于对当前的 DataFrame 就地做操作。

import pandas as pd

# Modify Person in place
def delete_duplicate_emails(person: pd.DataFrame) -> None:
    person.sort_values('id', inplace=True)
    person.drop_duplicates(subset=['email'], keep='first', inplace=True)

15 每个产品在不同商店的价格

1795. 每个产品在不同商店的价格

解答

import pandas as pd

def reconstruct(products: pd.DataFrame, col: str) -> pd.DataFrame:
    # df = products[['product_id', col]].dropna()
    df = products.loc[products[col].notna(), ['product_id', col]]
    df['price'] = df[col]
    df[col] = col
    df.rename(columns={col: 'store'}, inplace=True)
    return df

def rearrange_products_table(products: pd.DataFrame) -> pd.DataFrame:
    a = reconstruct(products, 'store1')
    b = reconstruct(products, 'store2')
    c = reconstruct(products, 'store3')
    df = pd.concat([a, b, c])
    return df

16 富有客户的数量

2082. 富有客户的数量

解答

import pandas as pd

def count_rich_customers(store: pd.DataFrame) -> pd.DataFrame:
    gb = store.groupby('customer_id')['amount'].max()
    gb = gb[gb > 500]
    df = pd.DataFrame({'rich_count': [len(gb)]})
    return df

官方题解给的是,直接对 store 进行布尔索引,然后使用 nuique() 计数

17 即时食物配送 I

1173. 即时食物配送 I

解答

保留小数使用的是 round(float, n) 方法

import pandas as pd

def food_delivery(delivery: pd.DataFrame) -> pd.DataFrame:
    immediate = (delivery.order_date == delivery.customer_pref_delivery_date)
    percent = len(delivery[immediate]) / len(delivery) * 100
    percent = round(percent, 2)
    df = pd.DataFrame({'immediate_percentage': [percent] })    
    return df

18 按分类统计薪水

1907. 按分类统计薪水

解答

需要注意计算 mid 时,需要注意运算顺序,& 的两边要有括号

import pandas as pd

def count_salary_categories(accounts: pd.DataFrame) -> pd.DataFrame:
    low = (accounts.income < 20000).sum()
    mid = ((accounts.income <= 50000) & (accounts.income >= 20000)).sum()
    high = (accounts.income > 50000).sum()
    df = pd.DataFrame({
        'category': ['Low Salary', 'Average Salary', 'High Salary'],
        'accounts_count': [low, mid, high]
    })
    return df

19 查找每个员工花费的总时间

1741. 查找每个员工花费的总时间

解答

这个题可以用来熟悉下对 apply 的使用,它可以对 pandas 中的数据结构进行高效的迭代

import pandas as pd

def total_time(employees: pd.DataFrame) -> pd.DataFrame:
    gb = employees.groupby(['emp_id', 'event_day'], as_index=False)
    gb = gb.agg({
        'in_time': np.sum,
        'out_time': np.sum
        }).rename(columns={'event_day': 'day'})
    df = gb[['day', 'emp_id']]
    df['total_time'] =  gb.apply(lambda x: x['out_time'] - x['in_time'], axis=1)
    return df

20 游戏玩法分析 I

511. 游戏玩法分析 I

解答

这个题注意下 groupby 索引操作对象时,列表索引和元素索引也是有区别的。和普通的一样,列表索引返回的是 DataFrame,元素索引返回的是 Series

import pandas as pd

def game_analysis(activity: pd.DataFrame) -> pd.DataFrame:
    df = activity.groupby('player_id', as_index=False)[['event_date']].min().rename(columns={
        'event_date': 'first_login'
        })
    return df[['player_id', 'first_login']]

21 每位教师所教授的科目种类的数量

2356. 每位教师所教授的科目种类的数量

解答

import pandas as pd

def count_unique_subjects(teacher: pd.DataFrame) -> pd.DataFrame:
    df = teacher.groupby('teacher_id', as_index=False)[['subject_id']].nunique()
    df.rename(columns={'subject_id': 'cnt'}, inplace=True)
    return df

22 超过5名学生的课

596. 超过5名学生的课

解答

import pandas as pd

def find_classes(courses: pd.DataFrame) -> pd.DataFrame:
    df = courses.groupby('class', as_index=False).nunique()
    index = df[df['student'] >= 5]['class']
    df = pd.DataFrame(data={'class': index.values})
    return df

23 订单最多的客户

586. 订单最多的客户

解答

import pandas as pd

def largest_orders(orders: pd.DataFrame) -> pd.DataFrame:
    gb = orders.groupby('customer_number', as_index=False).size()
    id = gb.customer_number[gb['size'] == gb['size'].max()]
    return pd.DataFrame(data={'customer_number': id})
    

24 按日期分组销售产品

1484. 按日期分组销售产品

解答

这里主要是应用 tolist 这个函数,然后学习下 named aggregation

import pandas as pd

def categorize_products(activities: pd.DataFrame) -> pd.DataFrame:
    df = activities.drop_duplicates().groupby('sell_date', as_index=False).agg(
        num_sold = ('product', 'nunique'),
        products = ('product', lambda x: sorted(x.tolist()))
    )
    return df.sort_values('sell_date')

25 每天的领导和合伙人

1693. 每天的领导和合伙人

解答

import pandas as pd

def daily_leads_and_partners(daily_sales: pd.DataFrame) -> pd.DataFrame:
    df = daily_sales.groupby(['date_id', 'make_name'], as_index=False).agg(
        unique_leads = ('lead_id', 'nunique'),
        unique_partners = ('partner_id', 'nunique')
    )
    return df

26 合作过至少三次的演员和导演

1050. 合作过至少三次的演员和导演

解答

import pandas as pd

def actors_and_directors(actor_director: pd.DataFrame) -> pd.DataFrame:
    df = actor_director.groupby(['actor_id', 'director_id'], as_index=False).size()
    df = df.loc[df['size'] >= 3][['actor_id', 'director_id']]
    return df

27 使用唯一标识码替换员工ID

1050. 合作过至少三次的演员和导演

解答

import pandas as pd

def replace_employee_id(employees: pd.DataFrame, employee_uni: pd.DataFrame) -> pd.DataFrame:
    df = employees.merge(employee_uni, on='id', how='left')
    return df[['unique_id', 'name']]

28 学生们参加各科测试的次数

1280. 学生们参加各科测试的次数

解答

这个题涉及到一种连接方式 how='cross',这相当于做笛卡尔积

import pandas as pd

def students_and_examinations(students: pd.DataFrame, subjects: pd.DataFrame, examinations: pd.DataFrame) -> pd.DataFrame:
    all_sub = pd.merge(students, subjects, how='cross')
    count = examinations.groupby(['student_id', 'subject_name'], as_index=False).size().rename(columns={'size': "attended_exams"})
    df = pd.merge(all_sub, count, on=['student_id', 'subject_name'], how='left')
    df['attended_exams'] = df['attended_exams'].fillna(0).astype(int)
    df.sort_values(['student_id', 'subject_name'], inplace=True)
    return df[['student_id', 'student_name', 'subject_name', 'attended_exams']]

29 至少有5名直接下属的经理

570. 至少有5名直接下属的经理

解答

这里主要是要想到求交集就是用 inner 的连接

import pandas as pd

def find_managers(employee: pd.DataFrame) -> pd.DataFrame:
    df = employee.dropna().groupby('managerId', as_index=False).size()
    df = df[df['size'] >= 5]
    name = pd.merge(employee, df, left_on='id', right_on='managerId', how='inner')[['name']]
    return name

30 销售员

607. 销售员

解答

这个题要用 isin 去排除得到 notin,不能直接找 name != 'RED'

import pandas as pd

def sales_person(sales_person: pd.DataFrame, company: pd.DataFrame, orders: pd.DataFrame) -> pd.DataFrame:
    df = pd.merge(company, orders, on='com_id', how='right')
    df = df.loc[df['name'] == 'RED',['sales_id']]
    df = sales_person[~sales_person['sales_id'].isin(df['sales_id'])][['name']]
    return df