如何制作好的可复现的pandas示例

226
花了相当一段时间观察SO上的标签后,我得到的印象是,pandas的问题很少包含可重现的数据。这是R社区一直以来鼓励的事情,多亏了像this这样的指南,新手们能够得到一些关于如何组织这些示例的帮助。那些能够阅读这些指南并提供可重现数据的人往往更容易得到问题的答案。
我们如何为pandas的问题创建好的可重现示例呢?可以简单地组合数据框,例如:
import pandas as pd
df = pd.DataFrame({'user': ['Bob', 'Jane', 'Alice'], 
                   'income': [40000, 50000, 42000]})

但是许多示例数据集需要更复杂的结构,例如:
- `datetime` 索引或数据 - 多个分类变量(是否有类似于 R 的 `expand.grid()` 函数的等价物,可以生成给定变量的所有可能组合?) - 多级索引数据
对于那些难以用几行代码模拟的数据集,是否有类似于 R 的 `dput()` 的等价物,允许您生成可复制粘贴的代码以重新生成数据结构?

8
如果你复制输出内容,大多数回答者可以使用read_clipboard()...除了MultiIndex。话说,字典是个很好的补充。 - Andy Hayden
8
除了Andy说的之外,我认为复制粘贴 df.head(N).to_dict() 是一个不错的选择,其中 N 是一个合理的数字。如果添加漂亮的换行符到输出中会得到额外的加分。对于时间戳,通常只需要在代码顶部添加 from pandas import Timestamp 即可。 - Paul H
5个回答

481
注意:这里的大部分想法都对于Stack Overflow,以及一般的问题来说都是相当通用的。请参考最小、可复现的示例简短、自包含、正确的示例
免责声明:提出一个好问题是困难的。
好的方面:
请包含一个小的示例DataFrame,可以是可运行的代码:
```python In [1]: df = pd.DataFrame([[1, 2], [1, 3], [4, 6]], columns=['A', 'B']) ```
或者使用`pd.read_clipboard(sep='\s\s+')`使其可以“复制和粘贴”。自己测试一下。
```python In [2]: df Out[2]: A B 0 1 2 1 1 3 2 4 6 ```
你可以通过高亮并使用Ctrl+K(或在每行前面添加四个空格)来格式化文本以适应Stack Overflow,或者在你的代码上方和下方使用三个反引号(```)来使你的代码不缩进。
我真的是指一个的DataFrame。绝大多数示例DataFrame可能少于6行[需要引用],而且我敢打赌我可以在5行内完成。你能用`df = df.head()`复现错误吗?如果不能,试着找到一个能展示你所面临问题的小DataFrame。
但是每个规则都有例外,明显的例外是性能问题(在这种情况下,一定要使用%timeit和可能的%prun来分析你的代码),在这种情况下,你应该生成:
```python df = pd.DataFrame(np.random.randn(100000000, 10)) ```
考虑使用`np.random.seed`,这样我们就有了完全相同的DataFrame。话虽如此,“让这段代码运行得更快”并不严格属于本站的主题。
对于可运行的代码,`df.to_dict`通常很有用,可以使用不同的`orient`选项处理不同的情况。在上面的示例中,我可以从`df.to_dict('split')`中获取数据和列。
写出你想要的结果(与上面类似):
```python In [3]: iwantthis Out[3]: A B 0 1 5 1 4 6 ```
解释数字的来源: > 5是B列中A为1的行的和。
展示你尝试过的代码:
```python In [4]: df.groupby('A').sum() Out[4]: B A 1 5 4 6 ```
但是指出错误之处: > A列在索引中而不是在列中。
展示你已经进行了一些研究(搜索文档搜索Stack Overflow),并给出一个总结: > sum的文档字符串只是简单地说明“计算组值的和”。 > groupby文档中没有给出任何示例。 顺便说一句,解决方法是使用`df.groupby('A', as_index=False).sum()`。 如果你的DataFrame中有时间戳列,例如你正在进行重新采样或其他操作,请明确地使用`pd.to_datetime`对它们进行处理。
```python df['date'] = pd.to_datetime(df['date']) # 这一列应该是日期。 ```
有时问题本身就是这个:它们是字符串。

不好的地方:

  • 不要包含一个MultiIndex,我们无法复制和粘贴(见上文)。这是Pandas默认显示的一个不满,但仍然很烦人:

    In [11]: df
    Out[11]:
         C
    A B
    1 2  3
      2  6
    

    正确的方法是使用一个普通的DataFrame,并调用set_index

    In [12]: df = pd.DataFrame([[1, 2, 3], [1, 2, 6]], columns=['A', 'B', 'C'])
    
    In [13]: df = df.set_index(['A', 'B'])
    
    In [14]: df
    Out[14]:
         C
    A B
    1 2  3
      2  6
    
  • 在给出你想要的结果时,请提供一些见解:

       B
    A
    1  1
    5  0
    

    明确说明你得到这些数字的方式(它们是什么)... 仔细检查它们是否正确。

  • 如果你的代码抛出错误,请包含完整的堆栈跟踪。如果太嘈杂,以后可以编辑掉。显示出错的行号和相应的代码行。

  • Pandas 2.0引入了一些更改,之前是Pandas 1.0,所以如果你得到了意外的输出,请包含版本信息:

    pd.__version__
    

    另外,你可能还想包含Python的版本、你的操作系统和其他库的版本。你可以使用pd.show_versions()session_info包(显示加载的库和Jupyter/IPython环境)。

缺点:

  • 不要链接到我们无法访问的CSV文件(最好根本不要链接到外部来源)。

    df = pd.read_csv('my_secret_file.csv') # 最好有很多解析选项
    

    大多数数据都是专有的,我们明白。编造类似的数据,看看是否能够重现问题(一些小的数据)。

  • 不要用模糊的语言解释情况,比如你有一个“大”的DataFrame,顺便提一下一些列名(确保不提及它们的数据类型)。尽量详细地描述一些在没有实际上下文的情况下完全没有意义的事情。想必没有人会读到这段话的末尾。

    长篇大论是不好的;用小例子更容易。

  • 在提出问题之前,不要包含10+(100+??)行的数据处理。

    请理解,我们在日常工作中已经看到了足够多的这种情况。我们想要帮助,但不要这样做...。 简化介绍,只展示在引起问题的步骤中相关的DataFrames(或它们的小版本)。


56
对于pd.read_clipboard(sep='\s\s+')技巧给予赞赏。当我在Stack Overflow上发布需要一个特殊但易于共享的数据帧的问题时,例如此处,我会在Excel中构建它,将其复制到剪贴板中,然后指示其他人也这样做。这样可以节省很多时间! - zelusp
2
“pd.read_clipboard(sep='\s\s+')” 这个建议似乎在使用 Python 远程服务器时不起作用,而大量数据集就存储在这里。 - user5359531
2
为什么要使用pd.read_clipboard(sep='\s\s+'),而不是更简单的pd.read_clipboard()(默认为s+)?前者至少需要2个空格字符,如果只有1个可能会出现问题(例如在@JohnE的答案中看到)。 - MarianD
5
@MarianD,\s\s+之所以如此流行,是因为在列名中通常只有一个空格,但多个空格较为罕见,而且Pandas输出时会很好地在列之间至少放置两个空格。由于这只是针对玩具/小型数据集的,因此它非常强大/适用于大多数情况。注意:如果使用制表符分隔,则情况会有所不同,虽然stackoverflow将制表符替换为空格,但如果您有一个tsv文件,则只需使用\t即可。 - Andy Hayden
4
我总是使用 pd.read_clipboard(),当有空格时,我会这样写:pd.read_clipboard(sep='\s+{2,}', engine='python') :P - U13-Forward
显示剩余13条评论

99

如何创建样本数据集

本文主要对AndyHayden的回答进行拓展,提供有关如何创建样本数据帧的示例。Pandas和(尤其是)NumPy提供了多种工具,使您可以使用仅几行代码就能创建出与真实数据集相似的数据集。

导入NumPy和Pandas后,如果您希望其他人能够精确地再现您的数据和结果,请务必提供一个随机种子。

import numpy as np
import pandas as pd

np.random.seed(123)

一个示例

这是一个展示各种操作的示例。从其中的子集中可以创建各种有用的样本数据框:

df = pd.DataFrame({

    # some ways to create random data
    'a':np.random.randn(6),
    'b':np.random.choice( [5,7,np.nan], 6),
    'c':np.random.choice( ['panda','python','shark'], 6),

    # some ways to create systematic groups for indexing or groupby
    # this is similar to R's expand.grid(), see note 2 below
    'd':np.repeat( range(3), 2 ),
    'e':np.tile(   range(2), 3 ),

    # a date range and set of random dates
    'f':pd.date_range('1/1/2011', periods=6, freq='D'),
    'g':np.random.choice( pd.date_range('1/1/2011', periods=365,
                          freq='D'), 6, replace=False)
    })

这会产生:

          a   b       c  d  e          f          g
0 -1.085631 NaN   panda  0  0 2011-01-01 2011-08-12
1  0.997345   7   shark  0  1 2011-01-02 2011-11-10
2  0.282978   5   panda  1  0 2011-01-03 2011-10-30
3 -1.506295   7  python  1  1 2011-01-04 2011-09-07
4 -0.578600 NaN   shark  2  0 2011-01-05 2011-02-27
5  1.651437   7  python  2  1 2011-01-06 2011-02-03

一些注意事项:

  1. np.repeatnp.tile(列 de)非常适用于以非常规律的方式创建组和索引。对于两列,可以使用它们轻松复制 r 的 expand.grid() ,但也更灵活,能够提供所有排列的子集。但是,对于三列或更多列,语法很快变得难以处理。
  2. 要想直接替换 R 的 expand.grid(),请参见Pandas cookbook 中的 itertools 解决方案或者在此处查看展示的 np.meshgrid 解决方案。这些都允许任意数量的维度。
  3. 你可以通过 np.random.choice 做很多事情。例如,在列 g 中,我们从 2011 年中随机选择了六个日期。此外,通过设置 replace=False,我们可以确保这些日期是唯一的——如果我们想将它作为具有唯一值的索引使用,这将非常方便。

虚假股票市场数据

除了对上面的代码进行子集处理外,您还可以进一步结合这些技术来实现几乎任何事情。例如,下面是一个简短的示例,它结合使用 np.tiledate_range 来为涵盖相同日期的 4 只股票创建样本股票数据:

stocks = pd.DataFrame({
    'ticker':np.repeat( ['aapl','goog','yhoo','msft'], 25 ),
    'date':np.tile( pd.date_range('1/1/2011', periods=25, freq='D'), 4 ),
    'price':(np.random.randn(100).cumsum() + 10) })

现在我们有一个包含100行数据的样本数据集(每个股票代码25个日期),但我们只使用了4行数据,这样其他人就可以轻松复制并重现它,而不需要复制和粘贴100行代码。如果有必要解释问题,您可以显示数据的子集:

>>> stocks.head(5)

        date      price ticker
0 2011-01-01   9.497412   aapl
1 2011-01-02  10.261908   aapl
2 2011-01-03   9.438538   aapl
3 2011-01-04   9.515958   aapl
4 2011-01-05   7.554070   aapl

>>> stocks.groupby('ticker').head(2)

         date      price ticker
0  2011-01-01   9.497412   aapl
1  2011-01-02  10.261908   aapl
25 2011-01-01   8.277772   goog
26 2011-01-02   7.714916   goog
50 2011-01-01   5.613023   yhoo
51 2011-01-02   6.397686   yhoo
75 2011-01-01  11.736584   msft
76 2011-01-02  11.944519   msft

4
好的回答。写完这个问题后,我实际上编写了一个非常简短、简单的 expand.grid() 实现,它包含在 pandas cookbook 中,你也可以在你的答案中包含它。你的答案展示了如何创建比我的 expand_grid() 函数更复杂的数据集,这很棒。 - Marius
这是一个非常有用的例子,我将以此为基础进行示例。非常感谢! - Serge de Gosson de Varennes

69

一个回答者的日记

我对提问的最佳建议是利用那些回答问题的人的心理。作为其中的一员,我可以深入了解我为什么回答某些问题而不回答其他问题。

动机

我回答问题有以下几个原因:

  1. Stackoverflow.com 对我来说是一个非常宝贵的资源。我想回报社区。
  2. 在努力回报社区的过程中,我发现这个网站比以前更强大。回答问题是一次学习经历,我喜欢学习。读这个其他资深回答者的回答和评论。这种互动让我感到开心。
  3. 我喜欢积分!
  4. 参考第3条。
  5. 我喜欢有趣的问题。

我的所有最纯粹的意图都很好,但我对回答1个问题或30个问题都有满足感。我选择回答哪些问题的驱动力在于最大化积分。

我也会花时间解决有趣的问题,但这种情况并不常见,也无法帮助需要解决非有趣问题的提问者。如果你想让我回答一个问题,最好的方法就是将问题直接摆在我的面前,尽可能地让我用最少的力气来回答它。如果我看到两个问题,其中一个有可复制粘贴的代码,可以创建我需要的所有变量...我会选择那个!如果有时间,我再回来看另一个问题。

主要建议

让回答问题的人更容易回答。

  • 提供创建所需变量的代码。
  • 尽量简化代码。如果我看到帖子时感到无聊,我会去看下一个问题或回到我正在做的事情。
  • 考虑你在问什么,并具体明确。我们想要看到你所做的,因为自然语言(英语)不够精确和易于混淆。你尝试过的代码示例有助于解决自然语言描述中的不一致性。
  • 请展示你期望的结果!!!我必须坐下来尝试一些东西。如果我没有看到你要找的示例,我可能会跳过这个问题,因为我不想猜测。

你的声誉不仅仅是你的声誉。

我喜欢分数(我之前提到过)。但是那些分数并不是我的真正声誉。我的真正声誉是其他网站用户对我的看法的综合体现。我努力做到公正和诚实,希望别人也能看到这一点。对于提问者来说,这意味着我们会记住提问者的行为。如果您不选择答案并赞同好的答案,我会记住的。如果您的行为方式不符合我的喜好或者符合我的喜好,我也会记住的。这也影响了我会回答哪些问题。


无论如何,我可能还可以继续下去,但我会节约那些真正阅读此内容的人的时间。


46

挑战 回答Stack Overflow问题最具挑战性的一个方面是重新创建问题(包括数据)所需的时间。那些没有明显方法来重现数据的问题很可能不会得到回答。考虑到你花时间编写问题并且有需要帮助解决的问题,你可以通过提供数据来帮助别人解决问题。

@Andy提供的有关编写良好Pandas问题的指南是一个很好的起点。更多信息,请参阅如何提问和如何创建Minimal, Complete, and Verifiable examples

请清楚地陈述您的问题。在编写完问题和任何示例代码后,请尝试阅读它并为您的读者提供一个“行政摘要”,总结问题并清楚地陈述问题。

原始问题:

我有这些数据...

我想做这件事...

我希望我的结果看起来像这样...

然而,当我尝试做[这个]时,我遇到了以下问题...

我已经尝试过通过做[这个]和[那个]来找到解决方案。

我该怎么办?

根据提供的数据量、示例代码和错误堆栈,读者需要花费很长时间才能理解问题所在。试着重新陈述您的问题,使问题本身处于顶部,然后提供必要的细节。

修订后的问题:

问题:如何做[这个]?

我已经尝试过通过做[这个]和[那个]来找到解决方案。

当我尝试做[这个]时,我遇到了以下问题...

我希望最终结果看起来像这样...

这是一些可以重现我的问题的最小代码...

这里是如何重新创建我的示例数据:df = pd.DataFrame({'A': [...], 'B': [...], ...})

提供样本数据(如果需要)!!!

有时仅需DataFrame的头或尾部即可。您还可以使用@JohnE提出的方法创建更大的数据集,其他人也可以重现它。使用他的示例生成一个100行的股票价格DataFrame:

stocks = pd.DataFrame({ 
    'ticker':np.repeat( ['aapl','goog','yhoo','msft'], 25 ),
    'date':np.tile( pd.date_range('1/1/2011', periods=25, freq='D'), 4 ),
    'price':(np.random.randn(100).cumsum() + 10) })

如果这是您实际的数据,您可能只想包括数据框的头部和/或尾部,如下所示(请确保对任何敏感数据进行匿名处理):

>>> stocks.head(5).to_dict()
{'date': {0: Timestamp('2011-01-01 00:00:00'),
  1: Timestamp('2011-01-01 00:00:00'),
  2: Timestamp('2011-01-01 00:00:00'),
  3: Timestamp('2011-01-01 00:00:00'),
  4: Timestamp('2011-01-02 00:00:00')},
 'price': {0: 10.284260107718254,
  1: 11.930300761831457,
  2: 10.93741046217319,
  3: 10.884574289565609,
  4: 11.78005850418319},
 'ticker': {0: 'aapl', 1: 'aapl', 2: 'aapl', 3: 'aapl', 4: 'aapl'}}

>>> pd.concat([stocks.head(), stocks.tail()], ignore_index=True).to_dict()
{'date': {0: Timestamp('2011-01-01 00:00:00'),
  1: Timestamp('2011-01-01 00:00:00'),
  2: Timestamp('2011-01-01 00:00:00'),
  3: Timestamp('2011-01-01 00:00:00'),
  4: Timestamp('2011-01-02 00:00:00'),
  5: Timestamp('2011-01-24 00:00:00'),
  6: Timestamp('2011-01-25 00:00:00'),
  7: Timestamp('2011-01-25 00:00:00'),
  8: Timestamp('2011-01-25 00:00:00'),
  9: Timestamp('2011-01-25 00:00:00')},
 'price': {0: 10.284260107718254,
  1: 11.930300761831457,
  2: 10.93741046217319,
  3: 10.884574289565609,
  4: 11.78005850418319,
  5: 10.017209045035006,
  6: 10.57090128181566,
  7: 11.442792747870204,
  8: 11.592953372130493,
  9: 12.864146419530938},
 'ticker': {0: 'aapl',
  1: 'aapl',
  2: 'aapl',
  3: 'aapl',
  4: 'aapl',
  5: 'msft',
  6: 'msft',
  7: 'msft',
  8: 'msft',
  9: 'msft'}}

您可能还希望提供有关DataFrame的描述(仅使用相关列)。这将使其他人更容易检查每列的数据类型并识别其他常见错误(例如,将日期作为字符串与datetime64和对象相比):

stocks.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 100 entries, 0 to 99
Data columns (total 3 columns):
date      100 non-null datetime64[ns]
price     100 non-null float64
ticker    100 non-null object
dtypes: datetime64[ns](1), float64(1), object(1)

注:如果您的DataFrame具有MultiIndex:

如果您的DataFrame具有多级索引,则必须先重置后才能调用to_dict。 然后,您需要使用set_index重新创建索引:

# MultiIndex example.  First create a MultiIndex DataFrame.
df = stocks.set_index(['date', 'ticker'])
>>> df
                       price
date       ticker           
2011-01-01 aapl    10.284260
           aapl    11.930301
           aapl    10.937410
           aapl    10.884574
2011-01-02 aapl    11.780059
...

# After resetting the index and passing the DataFrame to `to_dict`, make sure to use 
# `set_index` to restore the original MultiIndex.  This DataFrame can then be restored.

d = df.reset_index().to_dict()
df_new = pd.DataFrame(d).set_index(['date', 'ticker'])
>>> df_new.head()
                       price
date       ticker           
2011-01-01 aapl    10.284260
           aapl    11.930301
           aapl    10.937410
           aapl    10.884574
2011-01-02 aapl    11.780059

24
这是我为 Pandas 的 DataFrame 版本编写的 dput,它是生成可重复报告的标准 R 工具。对于更复杂的数据框架可能会出现问题,但在简单情况下似乎可以胜任:
import pandas as pd
def dput(x):
    if isinstance(x,pd.Series):
        return "pd.Series(%s,dtype='%s',index=pd.%s)" % (list(x),x.dtype,x.index)
    if isinstance(x,pd.DataFrame):
        return "pd.DataFrame({" + ", ".join([
            "'%s': %s" % (c,dput(x[c])) for c in x.columns]) + (
                "}, index=pd.%s)" % (x.index))
    raise NotImplementedError("dput",type(x),x)

现在,

df = pd.DataFrame({'a':[1,2,3,4,2,1,3,1]})
assert df.equals(eval(dput(df)))
du = pd.get_dummies(df.a,"foo")
assert du.equals(eval(dput(du)))
di = df
di.index = list('abcdefgh')
assert di.equals(eval(dput(di)))

请注意,这会产生比DataFrame.to_dict更冗长的输出,例如:

pd.DataFrame({
  'foo_1':pd.Series([1, 0, 0, 0, 0, 1, 0, 1],dtype='uint8',index=pd.RangeIndex(start=0, stop=8, step=1)),
  'foo_2':pd.Series([0, 1, 0, 0, 1, 0, 0, 0],dtype='uint8',index=pd.RangeIndex(start=0, stop=8, step=1)),
  'foo_3':pd.Series([0, 0, 1, 0, 0, 0, 1, 0],dtype='uint8',index=pd.RangeIndex(start=0, stop=8, step=1)),
  'foo_4':pd.Series([0, 0, 0, 1, 0, 0, 0, 0],dtype='uint8',index=pd.RangeIndex(start=0, stop=8, step=1))},
  index=pd.RangeIndex(start=0, stop=8, step=1))

对比

{'foo_1': {0: 1, 1: 0, 2: 0, 3: 0, 4: 0, 5: 1, 6: 0, 7: 1}, 
 'foo_2': {0: 0, 1: 1, 2: 0, 3: 0, 4: 1, 5: 0, 6: 0, 7: 0}, 
 'foo_3': {0: 0, 1: 0, 2: 1, 3: 0, 4: 0, 5: 0, 6: 1, 7: 0}, 
 'foo_4': {0: 0, 1: 0, 2: 0, 3: 1, 4: 0, 5: 0, 6: 0, 7: 0}}
对于上面的du命令,它可以保留列类型。例如,在上面的测试用例中,

请注意,此处的翻译不包含原始内容,只是对需要翻译的文本进行了翻译。

du.equals(pd.DataFrame(du.to_dict()))
==> False

因为 du.dtypesuint8,而 pd.DataFrame(du.to_dict()).dtypes 则是 int64


虽然我承认不明白为什么要使用它而不是 to_dict,但是这样做更清晰。 - Paul H
5
因为它保留了列类型。更具体地说,du.equals(eval(dput(df))) - sds
我喜欢这个。我有一个更现代的版本,使用插值字符串,并通过换行符分隔输出:def dput(x): indent = " " if isinstance(x,pd.Series): return f"pd.Series({list(x)},dtype='{x.dtype}',index=pd.{x.index}),\r\n" if isinstance(x,pd.DataFrame): temp = "pd.DataFrame({\r\n" + indent temp += indent.join([ f"'{c}': {dput(x[c])}" for c in x.columns]) temp += (f"}}, index=pd.{x.index})") return temp.replace("nan", "float(\'NaN\')") raise NotImplementedError("dput",type(x),x) - butterflyknife

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接