[문제]

https://leetcode.com/problems/immediate-food-delivery-ii/description/

 

[풀이]

1. 각 고객별로 첫주문을 가져오고, 해당 주문과 희망 배송 날짜가 같은 걸 찾고 더한 뒤 전체 개수로 나눠줌

 

Pandas

import pandas as pd

def immediate_food_delivery(delivery: pd.DataFrame) -> pd.DataFrame:
    delivery = delivery.groupby('customer_id').min()
    return pd.DataFrame({'immediate_percentage' : [round((delivery['order_date'] == delivery['customer_pref_delivery_date']).mean() * 100, 2)]})

 

SQL

SELECT ROUND(AVG(order_date=customer_pref_delivery_date)*100, 2) AS immediate_percentage
FROM Delivery
WHERE (customer_id, order_date) in (SELECT 
                                        customer_id
                                        , min(order_date)
                                    FROM Delivery
                                    GROUP BY customer_id
                                    )
SELECT 
    ROUND(SUM(order_date=customer_pref_delivery_date AND first_order=1) / COUNT(DISTINCT customer_id) * 100, 2) AS immediate_percentage 
FROM 
(
SELECT
    *
    , RANK() OVER(PARTITION BY customer_id ORDER BY order_date) AS first_order
FROM Delivery
) a

'코드카타 > Pandas' 카테고리의 다른 글

100. User Activity for the Past 30 Days I  (0) 2025.01.05
Game Play Analysis IV  (0) 2025.01.01
Monthly Transactions I  (0) 2024.12.30
Queries Quality and Percentage  (1) 2024.12.27
Percentage of Users Attended a Contest  (0) 2024.12.26

[문제] 

https://leetcode.com/problems/monthly-transactions-i/description/

 

[풀이]

1. state 컬럼이 'approved' 인 행에만 집계함수를 적용할 수 있도록 np.where 또는 apply 함수를 통해 새로운 컬럼을 생성

2.  month 컬럼을 요구된 날짜포맷으로 변경

3. month 컬럼과 country 컬럼으로 groupby 를 진행하는데 기준 컬럼이 null값 이어도 그룹화에서 제외하지 않도록dropna=False 설정

4. agg 함수를 적용하고 reset_index 함수로 마무리

 

Pandas

def monthly_transactions(transactions: pd.DataFrame) -> pd.DataFrame:
    transactions['approved'] = np.where(transactions['state']=='approved', transactions['amount'], np.nan)
    transactions['month'] = transactions['trans_date'].dt.strftime("%Y-%m")

    return transactions.groupby(['month', 'country'], dropna=False).agg(
        trans_count=('state','count')
        , approved_count=('approved','count')
        , trans_total_amount=('amount','sum')
        , approved_total_amount=('approved','sum')
    ).reset_index()

 

SQL

SELECT 
    DATE_FORMAT(trans_date, '%Y-%m') as month
    , country
    , COUNT(trans_date) AS trans_count
    , SUM(IF(state='approved', 1, 0)) AS approved_count
    , SUM(amount) AS trans_total_amount
    , SUM(IF(state='approved', amount, 0)) AS approved_total_amount
FROM Transactions
GROUP BY 1, 2

'코드카타 > Pandas' 카테고리의 다른 글

Game Play Analysis IV  (0) 2025.01.01
Immediate Food Delivery II  (0) 2024.12.31
Queries Quality and Percentage  (1) 2024.12.27
Percentage of Users Attended a Contest  (0) 2024.12.26
Project Employees I  (0) 2024.12.26

[문제]

https://leetcode.com/problems/queries-quality-and-percentage/description/

 

 

[풀이]

1. rating 컬럼과 position 컬럼을 나눈 새로운 컬럼을 생성한다. 

2. rating 컬럼이 3 미만인 조건을 통해 불리언 배열을 생성하고 이에 100을 곱해주는 새로운 컬럼을 생성한다. (비율이므로 미리 계산)

3. query_name으로 그룹화한 뒤 두 컬럼을 평균해준다. 반올림도 진행해주는데 아마 틀렸다고 나올 것이다.

4. 이는 판다스의 반올림에는 다른 규칙이 있기 때문인데, 예를들어 소수점 둘째짜리까지 반올림하는 경우 반올림할 숫자가 0.125라면 0.12로 반올림한다. 이는 편향을 줄이기 위한 방법인데 반올림할 값이 딱 중간인 5라면 가까운 짝수로 반올림한다. 그래서 이를 보정해주기 위해 아주 작은 값을 더해줘서 짝수가 아닌 일반적인 방식으로 반올림이 수행되도록 해준다.

 

Pandas

import pandas as pd

def queries_stats(queries: pd.DataFrame) -> pd.DataFrame:
    queries['quality'] = queries.rating / queries.position + 10e-9
    queries['poor_query_percentage'] = (queries.rating < 3) * 100
    return queries.groupby('query_name', as_index=False)[['quality', 'poor_query_percentage']].mean().round(2)

 

SQL

SELECT 
    query_name
    , ROUND(AVG(rating / position), 2) AS quality
    , ROUND(AVG(IF(rating < 3, 1, 0)) * 100, 2) AS poor_query_percentage
FROM Queries 
GROUP BY query_name

'코드카타 > Pandas' 카테고리의 다른 글

Immediate Food Delivery II  (0) 2024.12.31
Monthly Transactions I  (0) 2024.12.30
Percentage of Users Attended a Contest  (0) 2024.12.26
Project Employees I  (0) 2024.12.26
Average Selling Price  (0) 2024.12.25

[문제]

https://leetcode.com/problems/percentage-of-users-attended-a-contest/description/

 

 

[풀이]

1. register 테이블에서 contest_id 별로 유저 수를 계산한다.

2. 계산한 유저 수를 총 유저 수로 나누어서 비율(percentage을 구해준다.

3. percentage 를 내림차순 정렬, contest_id 를 오름차순 정렬 해준다.

 

Pandas

def users_percentage(users: pd.DataFrame, register: pd.DataFrame) -> pd.DataFrame:
    grouped = register.groupby('contest_id')['user_id'].count().reset_index(name='user_cnt')
    grouped['percentage'] = round(grouped['user_cnt'] / len(users) * 100, 2)
    grouped.sort_values(by=['percentage', 'contest_id'], ascending=[False, True], inplace=True)
    
    return grouped[['contest_id', 'percentage']]

 

SQL

SELECT 
    contest_id
    , ROUND(COUNT(user_id)/(SELECT COUNT(user_id) FROM Users)*100, 2) AS percentage 
FROM Register
GROUP BY contest_id
ORDER BY percentage DESC, contest_id

'코드카타 > Pandas' 카테고리의 다른 글

Monthly Transactions I  (0) 2024.12.30
Queries Quality and Percentage  (1) 2024.12.27
Project Employees I  (0) 2024.12.26
Average Selling Price  (0) 2024.12.25
Not Boring Movies  (0) 2024.12.25

[문제]

https://leetcode.com/problems/project-employees-i/description/

 

 

[풀이]

1. project 테이블을 기준으로 left join 을 해준다.

2. project_id 별로 experience_years의 평균을 구해주는데 소수점 둘째 자리까지 반올림 해준다.

3. 컬럼명을 average_years 로 변경해준다.

 

Pandas

import pandas as pd

def project_employees_i(project: pd.DataFrame, employee: pd.DataFrame) -> pd.DataFrame:
    
    return project.merge(employee, how='left').groupby('project_id')['experience_years'].mean().round(2).reset_index(name='average_years')

 

SQL

# Write your MySQL query statement below
SELECT p.project_id
       , ROUND(AVG(experience_years), 2) AS average_years
FROM Project p
LEFT JOIN Employee e
    ON p.employee_id = e.employee_id
GROUP BY p.project_id

'코드카타 > Pandas' 카테고리의 다른 글

Queries Quality and Percentage  (1) 2024.12.27
Percentage of Users Attended a Contest  (0) 2024.12.26
Average Selling Price  (0) 2024.12.25
Not Boring Movies  (0) 2024.12.25
Confirmation Rate  (0) 2024.12.24

[문제]

https://leetcode.com/problems/average-selling-price/description/

 

 

[풀이]

시계열 조인 문제로 두 가지 방법이 있다. 일반적인 조인 방법(merge)과 투 포인터 접근법을 사용한 방법(merge_asof)이다. 

 

일반적인 조인 방법은 모든 가능한 왼쪽-오른쪽 조합을 계산하는 것으로 O(M x N) 의 시간복잡도를 가지며, 이후 필터링 하는 것은 비용이 너무 크다.

 

반면, merge_asof는 O(N)의 투 포인터 접근법을 사용해 왼쪽과 오른쪽 데이터를 순회하는데, 이는 시간 정렬이 필요하지만, 시계열 데이터 처리에서는 일반적으로 시간 정렬을 필요로 하기 때문에 큰 문제가 되지 않는다.

 

왼쪽: prices 데이터프레임, 오른쪽: units_sold 데이터프레임

 

1. merge 조인 방법

먼저 일반적인 조인 방법으로 풀어보자. prices와 units_sold를 product_id 열을 키로 병합한다. product_id의 모든 고유값을 가져와야 하므로 left join 해준다.

merged = prices.merge(units_sold, how='left')

 

 

병합한 테이블로부터 purchase_date가 start_date와 end_date 사이에 있는 행을 필터링 해준다. 추가로 purchase_date가 빈값인 행도 필터링해준다.

cond1 = merged['purchase_date'].between(merged['start_date'], merged['end_date'])
cond2 = merged['purchase_date'].isna()
merged = merged[cond1 | cond2]

 

 

자 이제 병합과 필터링을 끝마친 데이터프레임이 완성됐다. product_id 열로 그룹화한 뒤 각 product_id 별 평단가를 구해보자.

def weighted_mean(df, value, weight):
    vs = df[value]
    ws = df[weight]
    if ws.sum() != 0:
        return round((vs * ws).sum() / ws.sum(), 2)
    else:
        return 0

merged.groupby('product_id').apply(weighted_mean, 'price', 'units', include_groups=False)

 

product_id로 그룹화된 데이터프레임을 product_id 별로 weighted_mean 함수에 던져준다. 그러면 함수 내부에서 우리가 구하고 싶은 평균단가를 구할 수 있다. units.sum()가 0인 product_id는 0을 반환한다. 아래는 전체 코드다.

def average_selling_price(prices: pd.DataFrame, units_sold: pd.DataFrame) -> pd.DataFrame:
    merged = prices.merge(units_sold, how='left')
    cond1 = merged['purchase_date'].between(merged['start_date'], merged['end_date'])
    cond2 = merged['purchase_date'].isna()
    merged = merged[cond1 | cond2]

    def weighted_mean(df, value, weight):
        vs = df[value]
        ws = df[weight]
        return round((vs * ws).sum() / ws.sum(), 2) if ws.sum() != 0 else 0
        
    return merged.groupby('product_id').apply(weighted_mean, 'price', 'units', include_groups=False).reset_index(name='average_price')

 

 

2. merge_asof 를 이용한 방법

해당 방법을 사용하기 위해서는 먼저 병합할 열을 정렬을 해주어야 한다.

prices = prices.sort_values('start_date')
units_sold = units_sold.sort_values('purchase_date')

왼쪽 prices, 오른쪽 units_sold

 

 

먼저 merge_asof 함수에 대해 알아보자.

pandas.merge_asof(left, right, on=None, left_on=None, right_on=None, by=None, left_by=None, right_by=None, tolerance=None, allow_exact_matches=True, direction='backward')

 

left, right :  병합할 두 데이터프레임이다.

on : 두 데이터프레임에서 병합할 공통 열 이름이다.

left_on, right on : on 대신 각 데이터프레임에서 사용할 열을 별도로 지정할 수 있다.

 by, left_by, right_by : 추가적인 그룹화 기준 열을 지정할 수 있다.

 

direction : 병합 방향을 설정한다. 

  • 'backward' : left 값보다 작은 right의 값 중 가장 가까운 값 (기본값)
  • 'forward' : left 값보다 큰 right의 값 중 가장 가까운 값
  • 'nearest' : 가장 가까운 값 (작거나 크거나 상관없음)

merge_asof 는 항상 left join 만 수행한다. 구매 데이터를 가지고 평단가를 구하는 것이기 때문에 units_sold 기준으로 병합한다.

 

 

우리가 해야하는 것은 start_date 열과 purchase_date 열을 키로 product_id 별로 비교해서 병합하는 것이다.

purchase_date  값이 start_date 보다 커야 하니 start_date이 purchase_date 값과 같거나 작은 값 중 가장 가까운 값을 가져오도록 해야한다. 

merged = pd.merge_asof(units_sold, prices, by='product_id', left_on='purchase_date', right_on='start_date')

 

 

내부적으로 어떻게 돌아간 것인지 한번 시각화해보자.

 

by 매개변수로 product_id 의 고윳값을 나누고 해당 그룹별로 start_date와 purchase를 정렬된 상태로 나열한다.

purchase_date 값보다 start_date 값이 작거나 같은 것 중 가까운 값을 선택해서 병합된다.

이런 매커니즘으로 병합이 진행된다.

 

 

아직 몇가지 작업이 남아있다. purchase_date가 큰 값 중 작은 값이라도 end_date 값보다 커진다면 제외해 주어야 한다. 위의 케이스에서는 그런 경우는 없지만 일반화를 위해 처리해주자.

badprice = merged['purchase_date'] > merged['end_date']
merged.loc[badprice, ['price', 'units']] = 0

 

 

앞서 일반 조인과 마찬가지로 데이터프레임을 요구에 맞게 만들어주면 된다.

def weighted_mean(df, value, weight):
        vs = df[value]
        ws = df[weight]
        return round((vs * ws).sum() / ws.sum(), 2) if ws.sum() != 0 else 0

merged = merged.groupby('product_id').apply(weighted_mean, 'price', 'units', include_groups=False).reset_index(name='average_price')

 

 

마지막으로 product_id 3인 그룹도 처리해주어야 한다. 해당 그룹은 구매 기록이 없으므로 average_price 를 0으로 처리해주어야 한다.

priceIds = set(prices['product_id'].unique())
soldIds = set(units_sold['product_id'].unique())
missingIds = priceIds.difference(soldIds)
fill = pd.DataFrame({'product_id': list(missingIds), 'average_price': [0]*len(missingIds)})

 

 

이제 두 테이블을 합쳐주자

 

pd.concat([merged, fill])

 

전체코드

def average_selling_price(prices: pd.DataFrame, units_sold: pd.DataFrame) -> pd.DataFrame:
    prices = prices.sort_values('start_date')
    units_sold = units_sold.sort_values('purchase_date')

    merged = pd.merge_asof(units_sold, prices, by='product_id', left_on='purchase_date', right_on='start_date')

    badprice = merged['purchase_date'] > merged['end_date']
    merged.loc[badprice, ['price', 'units']] = 0

    def weighted_mean(df, value, weight):
            vs = df[value]
            ws = df[weight]
            return round((vs * ws).sum() / ws.sum(), 2) if ws.sum() != 0 else 0

    merged = merged.groupby('product_id').apply(weighted_mean, 'price', 'units', include_groups=False).reset_index(name='average_price')

    priceIds = set(prices['product_id'].unique())
    soldIds = set(units_sold['product_id'].unique())
    missingIds = priceIds.difference(soldIds)
    fill = pd.DataFrame({'product_id': list(missingIds), 'average_price': [0]*len(missingIds)})

    return pd.concat([merged, fill])

 

 

'코드카타 > Pandas' 카테고리의 다른 글

Percentage of Users Attended a Contest  (0) 2024.12.26
Project Employees I  (0) 2024.12.26
Not Boring Movies  (0) 2024.12.25
Confirmation Rate  (0) 2024.12.24
Managers with at Least 5 Direct Reports // agg()와 query()  (0) 2024.12.23

[문제]

https://leetcode.com/problems/not-boring-movies/

 

 

[풀이]

1. 조건1) 모듈 연산으로 id가 홀수인 행 필터링

2. 조건2) != 연산으로 description이 boring이 아닌 행 필터링

3. rating으로 내림차순 정렬

 

Pandas

import pandas as pd

def not_boring_movies(cinema: pd.DataFrame) -> pd.DataFrame:
    cond1 = cinema['id'] % 2 == 1
    cond2 = cinema['description'] != 'boring'
    return cinema[cond1 & cond2].sort_values(by='rating', ascending=False)

 

SQL

# Write your MySQL query statement below
SELECT *
FROM Cinema
WHERE id % 2 = 1 AND description != 'boring'
ORDER BY rating DESC

 

 

 

 

'코드카타 > Pandas' 카테고리의 다른 글

Project Employees I  (0) 2024.12.26
Average Selling Price  (0) 2024.12.25
Confirmation Rate  (0) 2024.12.24
Managers with at Least 5 Direct Reports // agg()와 query()  (0) 2024.12.23
Students and Examinations  (0) 2024.12.22

[문제]

https://leetcode.com/problems/confirmation-rate/description/

 

 

[풀이]

1. 모든 user_id가 보여야 하므로 Signups 테이블을 기준으로 LEFT JOIN

2. action 열에서 confirmed인 행과 아닌 행들을 1과 0으로 나눠서 comfirmation_rate을 mean 함수로 손쉽게 구할 수 있음

3. user_id로 그룹화 해준뒤 mean과 round 함수 적용

 

Pandas

import pandas as pd

def confirmation_rate(signups: pd.DataFrame, confirmations: pd.DataFrame) -> pd.DataFrame:
    df = pd.merge(signups, confirmations, on='user_id', how='left')
    df['confirmation_rate'] = df['action'].apply(lambda x: 1 if x=='confirmed' else 0)
    return df.groupby('user_id', as_index=False)['confirmation_rate'].mean().round(2)

 

SQL

SELECT s.user_id
       , ROUND(AVG(IF(action='confirmed', 1, 0)), 2) as confirmation_rate
FROM Signups s
LEFT JOIN Confirmations c
    ON s.user_id = c.user_id
GROUP BY s.user_id

'코드카타 > Pandas' 카테고리의 다른 글

Average Selling Price  (0) 2024.12.25
Not Boring Movies  (0) 2024.12.25
Managers with at Least 5 Direct Reports // agg()와 query()  (0) 2024.12.23
Students and Examinations  (0) 2024.12.22
Employee Bonus  (0) 2024.12.22

[문제]

https://leetcode.com/problems/managers-with-at-least-5-direct-reports/description/

 

 

[풀이]

Pandas

import pandas as pd

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

    managers = employee.groupby('managerId', as_index=False)\
    .agg(reporting=('id', 'count'),).query('reporting >=5 ')['managerId']
    return employee[employee['id'].isin(managers)][['name']]

 

SQL

SELECT name
FROM Employee
WHERE id IN (SELECT managerId
            FROM Employee
            GROUP BY managerId
            HAVING COUNT(managerId) >= 5)

-- SELECT e.name
-- FROM Employee AS e 
-- INNER JOIN Employee AS m ON e.id=m.managerId
-- GROUP BY m.managerId
-- HAVING COUNT(m.managerId) >= 5

 

 

agg 함수와 query 함수에 대해 알아보자.

 

1. agg 함수

agg 함수는 집계 함수를 적용할 때 사용함. 데이터프레임이나 그룹화된 데이터에 대해 여러 집계 함수를 한 번에 적용할 수 있는 도구임.

 

주요 특징

1) 하나의 열에 여러 집계 함수 적용 가능

2) 여러 열에 서로 다른 집계 함수 적용 가능

3) 그룹화된(groupby()) 데이터와 함께 사용 가능

 

문법

DataFrame.agg(func=None, axis=0, *args, **kwrags)

 

func : 집계 함수 또는 함수의 리스트/딕셔너리. 함수는 문자열이나 python 함수로 전달 가능 ('mean' or np.mean)

axis : 0 이 열기준으로 기본값, 1은 행 기준

반환값은 집계된 결과를 포함하는 DataFrame 또는 Series

 

1) 단일 열에 여러 함수 적용하려면 함수를 리스트 형태로 전달한다. 

result = df['A'].agg(['sum', 'mean', 'max']) # 시리즈 반환
result = df[['A']].agg(['sum', 'mean', 'max']) # 데이터프레임 반환

 

시리즈라면 시리즈 형태로 반환하고 데이터프레임이면 데이터프레임 형태로 반환한다

 

 

2) 여러 열에 동일한 함수 적용

# 모든 열에 대해 'sum' 함수 적용
result = df.agg('sum') # 시리즈 반환 (index가 열)
result = df.agg(['sum']) # 데이터프레임 반환 (index가 함수)
result = df.agg(['sum', 'mean']) # 데이터프레임 반환 (index가 함수)

 

 

3) 여러 열에 다른 함수 적용

result = df.agg({'A': 'sum', 'B': 'mean'}) # 시리즈 반환
result = df.agg({'A': ['sum'], 'B': ['mean']}) # 데이터프레임 반환
result = df.agg({'A': ['sum', 'mean'], 'B': ['count','mean']}) # 데이터프레임 반환

 

{열이름 : 함수} 처럼 딕셔너리 형태로 전달하면 된다. 여러 함수를 전달하려면 딕셔너리 값에 리스트로 감싸서 전달해주면 된다.

 

 

4) 그룹화된 데이터에 사용

# group 열을 그룹화한 후 value 열에 sum과 mean 함수 적용
result = df.groupby('group').agg({'value': ['sum', 'mean']})

 

 

 

5) 여러 함수 적용 결과에 새로운 이름 지정 

# value 열에 sum 함수를 적용한 열의 이름을 total_sum 로 한다
# value 열에 mean 함수를 적용한 열의 이름을 avg_value 로 한다
result = df.groupby('group').agg(total_sum=('value', 'sum'),
                                 avg_value=('value', 'mean'))

 

 

2. query 함수

문자열 기반으로 데이터프레임을 필터링할 수 있는 방법을 제공함 (SQL 스타일의 조건문 사용)

 

문법

DataFrame.query(expr, inplace=False, **kwargs)

 

expr : 필터링 조건을 나타내는 문자열. SQL과 비슷한 형식으로 작성

반환값은 조건에 맞는 행만 포함하는 새로운 데이터프레임

 

1. 조건문으로 필터링

# A가 15보다 큰 행 필터링
filtered_df = df.query("A > 15")

 

 

2. 여러 조건 사용

# A가 15보다 크고 B가 20보다 작은 행
filtered_df = df.query("A > 15 and B < 20")

 

 

3. 문자열 조건

# 이름이 Alice인 행
filtered_df = df.query("Name == 'Alice'")

 

컬럼 이름에 공백이나 특수문자가 있으면 컬럼명을 백틱(`) 으로 묶어서 사용해야 함

 

 

 

'코드카타 > Pandas' 카테고리의 다른 글

Not Boring Movies  (0) 2024.12.25
Confirmation Rate  (0) 2024.12.24
Students and Examinations  (0) 2024.12.22
Employee Bonus  (0) 2024.12.22
Average Time of Process per Machine  (0) 2024.12.21

[문제]

https://leetcode.com/problems/students-and-examinations/description/?source=submission-ac

 

[풀이]

cross로 조인한 테이블과 미리 집계한 테이블을 이어 붙일 생각을 할 수 있어야 풀 수 있는 문제다.

판다스로 문제를 풀 때는 테이블을 무작정 이을 생각이 아니라 처리를 먼저 한 다음에 붙일 줄 알아야 한다.

 

Pandas

import pandas as pd

def students_and_examinations(students: pd.DataFrame, subjects: pd.DataFrame, examinations: pd.DataFrame) -> pd.DataFrame:
    merged = pd.merge(students, subjects, how='cross')

    exam_count = examinations.groupby(['student_id', 'subject_name']).agg(attended_exams=('subject_name', 'count')).reset_index()

    result = merged.merge(exam_count, on=['student_id', 'subject_name'], how='left').sort_values(by=['student_id', 'subject_name'])
    result['attended_exams'] = result['attended_exams'].fillna(0)

    return result

 

SQL

# Write your MySQL query statement below
SELECT stu.student_id
    , student_name
    , sub.subject_name
    , COUNT(ex.subject_name) as attended_exams
FROM Students stu
JOIN Subjects sub
LEFT JOIN Examinations ex
    ON stu.student_id = ex.student_id
    AND sub.subject_name = ex.subject_name
GROUP BY 1, 2, 3
ORDER BY 1, 3

'코드카타 > Pandas' 카테고리의 다른 글

Confirmation Rate  (0) 2024.12.24
Managers with at Least 5 Direct Reports // agg()와 query()  (0) 2024.12.23
Employee Bonus  (0) 2024.12.22
Average Time of Process per Machine  (0) 2024.12.21
Rising Temperature  (0) 2024.12.21

+ Recent posts