Как перенести повторяющиеся столбцы в строки Панды

#python #pandas #transpose #melt

Вопрос:

У меня есть df1, который выглядит так:

 year    site    1   2   3   year    site    1   2   3   year    site    1   2   3   year    site    1   2   3
1991    A   4.1 5.9 4.1 1991    B   3.3 4.1 4.1 1991    C   4.1 0.6 4.1 1991    D   4.1 4.1 4.1
1992    A   6.2 5.7 6.2 1992    B   6.2 7.1 6.2 1992    C   6.2 6.2 6.2 1992    D   6.2 9.5 7.4
1993    A   2.6 1.9 4.7 1993    B   2.6 6.2 2.6 1993    C   5.4 8.3 2.6 1993    D   0.4 2.6 2.6
 

И у меня возникли проблемы с переносом столбцов, которые являются месяцами (1,2,3), в строки для каждого сайта, чтобы мой измененный df1 или df2 выглядел так:

 year    month   Site A  Site B  Site C  Site D
1991    1       4.1     3.3     4.1     4.1
1991    2       5.9     4.1     0.6     4.1
1991    3       4.1     4.1     4.1     4.1
1992    1       6.2     6.2     6.2     6.2
1992    2       5.7     7.1     6.2     9.5
1992    3       6.2     6.2     6.2     7.4
1993    1       2.6     2.6     5.4     0.4
1993    2       1.9     6.2     8.3     2.6
1993    3       4.7     2.6     2.6     2.6
 

Я пробовал использовать «расплав» и «стек», но я не понимаю, как ссылаться на повторяющиеся месяцы (1,2,3). Спасибо,

Комментарии:

1. пожалуйста, предоставьте воспроизводимый образец кадра данных

2. хорошо, конечно — я добавил исходные данные и надеюсь, что этого будет достаточно.

Ответ №1:

Попробуйте выполнить следующее, используя нарезку и изменение формы жесткого индекса:

 #Create input dataframe
np.random.seed(0)
df = pd.concat([pd.DataFrame({'year':[1991, 1992, 1993],
                  'site':[i]*3,
                  1:np.round(np.random.randint(2,8,3) np.random.random(3),1),
                  2:np.round(np.random.randint(2,8,3) np.random.random(3),1),
                  3:np.round(np.random.randint(2,8,3) np.random.random(3),1)}) for i in [*'ABC']], axis=1)

# index slice columns of the dataframe
df_out = pd.concat([df.iloc[:,i:i 5] for i in range(0,df.shape[1],5)])

# Reshape with melt, set_index, and unstack
df_out =  df_out.melt(['year', 'site'], var_name='month')
      .set_index(['year', 'month', 'site'])['value']
      .unstack('site').add_prefix('Site ')
      .reset_index()

print(df_out)
 

Выход:

 site  year  month  Site A  Site B  Site C
0     1991      1     6.6     6.0     5.5
1     1991      2     7.3     5.5     7.6
2     1991      3     3.9     2.5     4.7
3     1992      1     7.5     2.1     5.6
4     1992      2     4.1     2.8     7.9
5     1992      3     2.1     3.8     2.1
6     1993      1     2.4     5.9     4.0
7     1993      2     6.3     3.1     2.7
8     1993      3     3.1     3.1     3.7
 

Комментарии:

1. Я собирался сделать позиционный, но я не хотел так сильно полагаться на структуру столбцов XD

2. Да, разумно не делать этого таким образом, если вы не уверены, что ваша структура данных исправна и безопасна.

3. Хотя это намного проще, и у него есть преимущество в том, что он рано выходит из строя, если ваши данные должны быть однородными, но не по какой-либо причине.

Ответ №2:

Мы можем создать новый уровень столбцов, с помощью которого заголовок каждого столбца будет сгруппирован позиционно groupby cumcount . Преимущество этого заключается в том, что столбцы не должны находиться в определенном порядке, пока они имеют одинаковые имена.

Затем используйте stack , чтобы собрать все отдельные группы в строки, set_index исключить столбцы сайт и год, а затем stack и unstack сгруппировать по сайту вместо месяца:

 # calculate new MultiIndex level
midx = pd.MultiIndex.from_arrays([
    df.columns,
    df.columns.to_series().groupby(level=0).cumcount()
])
new_df = (
    df.set_axis(midx, axis=1)  # replace columns
        .stack()  # Move all groups into rows
        .set_index(['site', 'year'])  # save site and year
        .rename_axis(columns='Month')  # rename column axis to Month
        .stack()  # Move all month columns to rows
        .unstack(level='site')  # Convert to site rows to columns
        .add_prefix('Site ')  # Add Prefix
        .rename_axis(columns=None)  # Remove Axis Name
        .reset_index()  # Restore Range Index
)
 

new_df :

    year  Month Site A Site B Site C Site D
0  1991      1     A1     B1     C1     D1
1  1991      2     A2     B2     C2     D2
2  1991      3     A3     B3     C3     D3
3  1992      1     A1     B1     C1     D1
4  1992      2     A2     B2     C2     D2
5  1992      3     A3     B3     C3     D3
6  1993      1     A1     B1     C1     D1
7  1993      2     A2     B2     C2     D2
8  1993      3     A3     B3     C3     D3
 

Более рискованный подход заключается в том, чтобы reshape DataFrame.values основываться на заданном количестве уникальных столбцов ( 5 ), тогда все остальное будет таким же, как указано выше:

 unique_cols = df.columns.unique().tolist()
new_df = (
    pd.DataFrame(
        # reshape dataframe into len(unique_cols) columns
        # and however many rows
        df.values.reshape((-1, len(unique_cols))),
        columns=unique_cols  # restore column names
    ).set_index(['year', 'site'])
        .rename_axis(columns='Month')  # rename column axis to Month
        .stack()  # Move all month columns to rows
        .unstack(level='site')  # Convert to site rows to columns
        .add_prefix('Site ')  # Add Prefix
        .rename_axis(columns=None)  # Remove Axis Name
        .reset_index()  # Restore Range Index
)
 

new_df :

    year  Month Site A Site B Site C Site D
0  1991      1     A1     B1     C1     D1
1  1991      2     A2     B2     C2     D2
2  1991      3     A3     B3     C3     D3
3  1992      1     A1     B1     C1     D1
4  1992      2     A2     B2     C2     D2
5  1992      3     A3     B3     C3     D3
6  1993      1     A1     B1     C1     D1
7  1993      2     A2     B2     C2     D2
8  1993      3     A3     B3     C3     D3
 

*Обратите внимание, что этот подход работает только в том случае, если структура фрейма данных может быть гарантирована, поскольку мы обходим все проверки целостности данных pandas путем изменения формы с помощью numpy.


Используемая Настройка:

 from itertools import chain

import pandas as pd

sites = "ABCD"
df = pd.DataFrame(
    chain.from_iterable([range(1991, 1994),
                         [f'{v}'] * 3,
                         [f'{v}1'] * 3,
                         [f'{v}2'] * 3,
                         [f'{v}3'] * 3] for v in sites)
).T
df.columns = ['year', 'site', 1, 2, 3] * len(sites)
 

Сокращенный df :

    year site   1   2   3  year site   1  ...   1   2   3  year site   1   2   3
0  1991    A  A1  A2  A3  1991    B  B1  ...  C1  C2  C3  1991    D  D1  D2  D3
1  1992    A  A1  A2  A3  1992    B  B1  ...  C1  C2  C3  1992    D  D1  D2  D3
2  1993    A  A1  A2  A3  1993    B  B1  ...  C1  C2  C3  1993    D  D1  D2  D3
 

Ответ №3:

Решение @HenryEcker является правильным и предпочтительным, особенно когда столбцы не структурированы так, как описано выше.

В приведенном ниже решении используется pivot_longer от pyjanitor, и предполагается, что столбцы упорядочены (если вы не уверены в порядке, решение @HenryEcker безопасно и выполняет работу с помощью уникальной идеи cumount):

 # using Henry's data
# pip install pyjanitor
import janitor
import pandas as pd

df = df.rename(columns = str)
unique_columns = [*df.columns.unique()

(df.pivot_longer(names_to = unique_columns], 
                 names_pattern = unique_columns)
   .pivot('year', 'site')
   .stack(level = 0)
   .add_prefix('Site')
   .rename_axis(columns = None, 
                index = ['year', 'month'])
   .reset_index()
)
 
   year month SiteA SiteB SiteC SiteD
0  1991     1    A1    B1    C1    D1
1  1991     2    A2    B2    C2    D2
2  1991     3    A3    B3    C3    D3
3  1992     1    A1    B1    C1    D1
4  1992     2    A2    B2    C2    D2
5  1992     3    A3    B3    C3    D3
6  1993     1    A1    B1    C1    D1
7  1993     2    A2    B2    C2    D2
8  1993     3    A3    B3    C3    D3