【问题标题】:For each unique value in a pandas DataFrame column, how can I randomly select a proportion of rows?对于 pandas DataFrame 列中的每个唯一值,如何随机选择一定比例的行?
【发布时间】:2021-12-04 08:26:14
【问题描述】:

这里是 Python 新手。 想象一个看起来像这样的 csv 文件:

(……除了现实生活中,Person 列有 20 个不同的名字,每个 Person 有 300-500 行。另外,还有多个数据列,而不仅仅是一个。)

我想做的是随机标记每个 Person 行的 10% 并将其标记在新列中。我想出了一个非常复杂的方法来做到这一点——它包括创建一个随机数的辅助列和各种不必要的复杂的猜谜游戏。它奏效了,但很疯狂。最近,我想出了这个:

import pandas as pd 
df = pd.read_csv('source.csv')
df['selected'] = ''

names= list(df['Person'].unique())  #gets list of unique names

for name in names:
     df_temp = df[df['Person']== name]
     samp = int(len(df_temp)/10)   # I want to sample 10% for each name
     df_temp = df_temp.sample(samp)
     df_temp['selected'] = 'bingo!'   #a new column to mark the rows I've randomly selected
     df = df.merge(df_temp, how = 'left', on = ['Person','data'])
     df['temp'] =[f"{a} {b}" for a,b in zip(df['selected_x'],df['selected_y'])]
        #Note:  initially instead of the line above, I tried the line below, but it didn't work too well:
        #df['temp'] = df['selected_x'] + df['selected_y']
     df = df[['Person','data','temp']]
     df = df.rename(columns = {'temp':'selected'})

df['selected'] = df['selected'].str.replace('nan','').str.strip()  #cleans up the column

如您所见,基本上我为每个人提取了一个临时数据帧,使用DF.sample(number) 进行随机化,然后使用DF.merge 将“标记”行重新放入原始数据帧。它涉及遍历列表以创建每个临时 DataFrame...我的理解是迭代有点蹩脚。

必须有一种更 Pythonic、矢量化的方式来做到这一点,对吧?无需迭代。也许涉及groupby 的事情?非常感谢任何想法或建议。

编辑:这是另一种避免merge...但它仍然很笨重的方法:

import pandas as pd
import math
    
   #SETUP TEST DATA:
    y = ['Alex'] * 2321 + ['Doug'] * 34123  + ['Chuck'] * 2012 + ['Bob'] * 9281 
    z = ['xyz'] * len(y)
    df = pd.DataFrame({'persons': y, 'data' : z})
    df = df.sample(frac = 1) #shuffle (optional--just to show order doesn't matter)
    percent = 10  #CHANGE AS NEEDED
    
    #Add a 'helper' column with random numbers
    df['rand'] = np.random.random(df.shape[0])
    df = df.sample(frac=1)  #this shuffles data, just to show order doesn't matter
    
    #CREATE A HELPER LIST
    helper = pd.DataFrame(df.groupby('persons'['rand'].count()).reset_index().values.tolist()
    for row in helper:
        df_temp = df[df['persons'] == row[0]][['persons','rand']]
        lim = math.ceil(len(df_temp) * percent*0.01)
        row.append(df_temp.nlargest(lim,'rand').iloc[-1][1])
               
    def flag(name,num):
        for row in helper:
            if row[0] == name:
                if num >= row[2]:
                    return 'yes'
                else:
                    return 'no'
    
    df['flag'] = df.apply(lambda x: flag(x['persons'], x['rand']), axis=1)

【问题讨论】:

    标签: python pandas dataframe random vectorization


    【解决方案1】:

    如果我理解正确,您可以使用:

    df = pd.DataFrame(data={'persons':['A']*10 + ['B']*10, 'col_1':[2]*20})
    percentage_to_flag = 0.5
    a = df.groupby(['persons'])['col_1'].apply(lambda x: pd.Series(x.index.isin(x.sample(frac=percentage_to_flag, random_state= 5, replace=False).index))).reset_index(drop=True)
    df['flagged'] = a
    

    Input:

           persons  col_1
        0        A      2
        1        A      2
        2        A      2
        3        A      2
        4        A      2
        5        A      2
        6        A      2
        7        A      2
        8        A      2
        9        A      2
        10       B      2
        11       B      2
        12       B      2
        13       B      2
        14       B      2
        15       B      2
        16       B      2
        17       B      2
        18       B      2
        19       B      2
    

    Output with 50% flagged rows in each group:

         persons  col_1  flagged
    0        A      2    False
    1        A      2    False
    2        A      2     True
    3        A      2    False
    4        A      2     True
    5        A      2     True
    6        A      2    False
    7        A      2     True
    8        A      2    False
    9        A      2     True
    10       B      2    False
    11       B      2    False
    12       B      2     True
    13       B      2    False
    14       B      2     True
    15       B      2     True
    16       B      2    False
    17       B      2     True
    18       B      2    False
    19       B      2     True
    

    【讨论】:

    • 谢谢...这似乎非常正确,但是该代码没有给我上面显示的输出。相反,“a”是包含 2 个 ndarray 的系列,df 的新“标记”列包含第一行系列中的 1 个数组,第二行包含第二个数组,其余行 = nan。 (例如,df 列中的第一行 'flagged' = [True False False False True False False False True False]--大概因为 x.sample 每次运行都会改变。)不知道为什么它的行为不同对我来说,但这似乎已经非常接近了......
    • 如果您想在每次运行代码时生成相同的随机标志,您可以在x.sample() 中设置random_state = any_integer
    • 至于其他问题,对不起,代码中有一个小错误,我也编辑了。您现在可以尝试一下,它会为您生成与我设置随机状态相同的输出
    • 谢谢...这似乎适用于您创建的示例数据,但我认为当我更改数据时它不太适用。例如:y = ['Alex'] * 2321 + ['Doug'] * 34123 + ['Chuck'] * 2012 + ['Bob'] * 9281 /// z = ['xyz'] * len(y ) /// df = pd.DataFrame({'persons': y, 'data' : z})
    • a = df.groupby(['persons'])['data'].apply(lambda x: pd.Series(x.index.isin(x.sample(frac=percentage_to_flag, random_state= 5, replace=False).index))).reset_index(drop=True) 然后df['flagged'] = a。它对我来说工作正常
    【解决方案2】:

    您可以使用groupby.sample 来挑选整个数据帧的样本以进行进一步处理,或者识别数据帧的行以标记是否更方便。

    import pandas as pd
    
    percentage_to_flag = 0.5
    
    # Toy data: 8 rows, persons A and B.
    df = pd.DataFrame(data={'persons':['A']*4 + ['B']*4, 'data':range(8)})
    #   persons  data
    # 0       A     0
    # 1       A     1
    # 2       A     2
    # 3       A     3
    # 4       B     4
    # 5       B     5
    # 6       B     6
    # 7       B     7
    
    # Pick out random sample of dataframe.
    random_state = 41  # Change to get different random values.
    df_sample = df.groupby("persons").sample(frac=percentage_to_flag,
                                             random_state=random_state)
    #   persons  data
    # 1       A     1
    # 2       A     2
    # 7       B     7
    # 6       B     6
    
    # Mark the random sample in the original dataframe.
    df["marked"] = False
    df.loc[df_sample.index, "marked"] = True
    #   persons  data  marked
    # 0       A     0   False
    # 1       A     1    True
    # 2       A     2    True
    # 3       A     3   False
    # 4       B     4   False
    # 5       B     5   False
    # 6       B     6    True
    # 7       B     7    True
    

    如果您真的不想要子采样数据帧df_sample,您可以直接标记原始数据帧的样本:

    # Mark random sample in original dataframe with minimal intermediate data.
    df["marked2"] = False
    df.loc[df.groupby("persons")["data"].sample(frac=percentage_to_flag,
                                                random_state=random_state).index,
           "marked2"] = True
    #   persons  data  marked  marked2
    # 0       A     0   False    False
    # 1       A     1    True     True
    # 2       A     2    True     True
    # 3       A     3   False    False
    # 4       B     4   False    False
    # 5       B     5   False    False
    # 6       B     6    True     True
    # 7       B     7    True     True
    

    【讨论】:

    • 谢谢,听起来很有希望,但我得到“AttributeError: 'DataFrameGroupBy' object has no attribute 'sample'”。也许我可以调整以克服这个...
    • 可能是熊猫版本问题。也许会看到(this previous question)(stackoverflow.com/questions/36390406/…)。
    • 是的,我必须将其更改为:"df_sample = df.groupby("persons") /// df_sample = df_sample.apply(lambda x: x.sample(frac=percentage_to_flag,random_state= random_state))"...现在可以了。但现在我被困得更深了:“KeyError:“没有 [MultiIndex([('A', 1),\n ('A', 2),\n ('B', 5),\n ('B', 6)],\n names=['persons', None])] 在 [index]" 中,所以我必须弄清楚......但我想我可以通过添加来解决它: df_sample = df_sample.reset_index(level=0, drop=True)
    【解决方案3】:

    这是 TMBailey 的答案,经过调整,可以在我的 Python 版本中使用。 (不想编辑别人的答案,但如果我做错了,我会把它记下来。)这真的很棒而且非常快!

    编辑:我根据 TMBailey 的其他建议更新了此内容,将frac=percentage_to_flag 替换为n=math.ceil(percentage_to_flag * len(x))。这确保了舍入不会将采样的 %age 拉到 'percentage_to_flag' 阈值之下。 (对于它的价值,您也可以将其替换为frac=(math.ceil(percentage_to_flag * len(x)))/len(x))。

    import pandas as pd
    import math
    
    percentage_to_flag = .10
    
    # Toy data:
    y = ['Alex'] * 2321 + ['Eddie'] * 876 + ['Doug'] * 34123  + ['Chuck'] * 2012 + ['Bob'] * 9281 
    z = ['xyz'] * len(y)
    df = pd.DataFrame({'persons': y, 'data' : z})
    df = df.sample(frac = 1) #optional shuffle, just to show order doesn't matter
    
    # Pick out random sample of dataframe.
    random_state = 41  # Change to get different random values.
    df_sample = df.groupby("persons").apply(lambda x: x.sample(n=(math.ceil(percentage_to_flag * len(x))),random_state=random_state))
    #had to use lambda in line above
    df_sample = df_sample.reset_index(level=0, drop=True)  #had to add this to simplify multi-index DF
    
    # Mark the random sample in the original dataframe.
    df["marked"] = False
    df.loc[df_sample.index, "marked"] = True
    

    然后检查:

        pp = df.pivot_table(index="persons", columns="marked", values="data", aggfunc='count', fill_value=0)
        pp.columns = ['no','yes']
        pp = pp.append(pp.sum().rename('Total')).assign(Total=lambda d: d.sum(1))
        pp['% selected'] = 100 * pp.yes/pp.Total
        print(pp)
        
        OUTPUT:
                no   yes  Total  % selected
    persons                                
    Alex      2088   233   2321   10.038776
    Bob       8352   929   9281   10.009697
    Chuck     1810   202   2012   10.039761
    Doug     30710  3413  34123   10.002051
    Eddie      788    88    876   10.045662
    Total    43748  4865  48613   10.007611
    

    像魅力一样工作。

    【讨论】:

    • 当您调用x.sample 而不是传递frac 时,您可以传递类似n=math.ceil(percentage_to_flag * len(x)) 的内容。
    • 是的,这太棒了!而不是frac=percentage_to_flag,我可以使用frac=(math.ceil(percentage_to_flag * len(x)))/len(x),它们中的每一个都以> 10%的速度出现!谢谢——我会编辑/更新代码...
    • 当我查看df.sample 的帮助时,它描述了一个参数n 可以代替frac。当我查看源代码时,当给出frac 时,它无论如何都用于计算nn = round(frac * axis_length))。因此,如果您愿意,可以致电x.sample(n=...)。除非 pandas 版本还有其他问题?
    • 哦,是的,我明白你的意思了!我收集n 是一个数字,frac 是一个比例或分数——所以使用n 更简单,至少在这种情况下是这样。再次感谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-10-15
    • 2011-05-18
    • 2022-11-10
    • 2018-08-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多