30-days-of-pandas
题目来源 leetcode 上的 30 天 Pandas 挑战30 天 Pandas 挑战,本文作为一个打卡记录来学习 pandas 基本用法。
pandas 基础
主要从 Joyful Pandas 以及 Pandas 官方中文文档 上学习的 pandas
数据结构
首先是基本数据结构 Series
和 DataFrame
,其中 DataFrame
的每一行或每一列都是一个 Series
。基本的属性看名字就知道了:values, index, columns, dtypes, shape
,用于描述和展示的函数 info, describe, head, tail
基本函数
去重
unique
去除重复nunique
计算唯一值个数的函数value_counts
统计频次
但是 unique
只能用在 Series
上
duplicated
返回的是否为唯一值的布尔列表drop_duplicates
是去除这些重复的列
这两个函数都可以使用一个 keep
的参数,该参数决定保留重复的哪一项(默认是保留出现的第一次,keep='last'
则保留出现的最后一个,keep=false
则全不保留,去重时可以使用 subset
来决定考虑哪些列。
修改属性
修改索引名的函数 rename(columns=dict)
,其中字典的格式是 {'pre_name': 'new_name'}
排序
sort_values
根据属性值排序sort_index
根据索引值排序
默认 ascending=True
rank
方法是对某一属性计算排名,有多种排序方法,比如method='dense' 或者 'first'
等
转换类型
tolist
将 Series 转换成列表,因此也可以用于将 DataFrame 的一列转换成列表
pandas 索引
1. 序列 s
的索引
使用 s[item]
或者 s[一个列表]
2. DataFrame df
的列索引
使用 df[列名]
(等价于 df.列名
) 或者 df[列名的列表]
(以下不包含列表形式,同理即可)
3.df
的 loc
索引
使用 df.loc[*, *]
,其中 *
可以是元素,元素的列表,元素的切片,布尔列表及函数,第二个 *
可以省略,表示取所有列。主要讲后三个
loc 的切片和普通的切片不同,是包含两个端点的,比如 df.loc['yjj' : 'psy']
这个切片是包含 yjj
和 psy
这两行的
传入 loc
的布尔列表长度应该和 df
长度相同,然后选出布尔列表中为 true
的部分
使用函数时,返回值必须为元素,元素列表,元素切片或布尔列表。对于 df.loc[func]
事实上是 df.loc[func(df)]
一个细节是第二个 *
如果是单一的元素,比如 loc[:, 'age']
,则取出的是一个 Series,如果是元素列表,则取出的是 DataFrame,比如 loc[:, ['age']]
取出的是只有一列的 DataFrame
4. df
的 iloc
索引
这种索引方式和 loc
索引基本一致,除了 iloc
索引的是位置的序号,loc
索引的是元素。
索引运算
四种运算:intersection, union, difference, symmetric_difference
字符串
str
属性是对 Index
或 Series
上的字符串类型的访问器,可以批量处理数据,比如 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
可以帮助我们对多个列进行不同的聚合,对一个列进行多种聚合,支持聚合后的列命名
-
一列采用多种聚合
每一列都使用多种函数grouped.agg(['sum', 'mean'])
,这种方法进行聚合时,比如Age
列使用了mean
和max
两种统计方法,则生成的 DataFrame 是有多级索引的,这个例子中返回的分组 DataFrame 有这样的列| Age | |mean|max|
-
多列采用不同聚合
每一列都有自己的聚合规则grouped.agg({ 'math_score': ['mean', 'max'], 'physics_score': ['meam', 'min'] })
-
聚合后重命名
两种形式,第一种是在每一列选用的聚合函数的列表中,每个聚合函数改为一个元组,形式为(名字, 函数)
,比如'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
等
isna
和 notna
可以用于布尔索引
题目
1 大的国家
解答
这种使用布尔列表进行索引就好了,loc
和 iloc
都可以使用布尔索引,但是一般我们使用 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 可回收且低脂的产品
解答
这个题目 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 从不订购的客户
解答
这里需要找一个表的属性是否在另一个表中,使用的是 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
解答
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 无效的推文
解答
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 计算特殊奖金
解答
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 修复表中的名字
解答
需要注意的是,每个字符串方法前,需要使用 .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 查找拥有有效邮箱的用户
解答
这一题需要复习正则表达式
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 患某种疾病的患者
解答
这个题依然使用正则表达式解决,但是需要使用到单词边界的匹配。
import pandas as pd
def find_patients(patients: pd.DataFrame) -> pd.DataFrame:
target = r'\bDIAB1'
return patients[patients.conditions.str.contains(target)]
10 第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 第二高的薪水
解答
没搞懂,这和上一题一样的。改成 2 即可。
12 部门工资最高的员工
解答
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 分数排名
解答
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 删除重复的电子邮箱
解答
这里涉及到 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 每个产品在不同商店的价格
解答
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 富有客户的数量
解答
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
解答
保留小数使用的是 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 按分类统计薪水
解答
需要注意计算 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 查找每个员工花费的总时间
解答
这个题可以用来熟悉下对 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
解答
这个题注意下 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 每位教师所教授的科目种类的数量
解答
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名学生的课
解答
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 订单最多的客户
解答
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 按日期分组销售产品
解答
这里主要是应用 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 每天的领导和合伙人
解答
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 合作过至少三次的演员和导演
解答
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
解答
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 学生们参加各科测试的次数
解答
这个题涉及到一种连接方式 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名直接下属的经理
解答
这里主要是要想到求交集就是用 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 销售员
解答
这个题要用 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